tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  54from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74
  75class TinkoffBrokerServer:
  76    """
  77    This class implements methods to work with Tinkoff broker server.
  78
  79    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  80
  81    About `token`: https://tinkoff.github.io/investAPI/token/
  82    """
  83    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  84        """
  85        Main class init.
  86
  87        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  88        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  89                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  90        :param useCache: use default cache file with raw data to use instead of `iList`.
  91                         True by default. Cache is auto-update if new day has come.
  92                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  93        :param defaultCache: path to default cache file. `dump.json` by default.
  94        """
  95        if token is None or not token:
  96            try:
  97                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  98                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
  99
 100            except KeyError:
 101                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 102                raise Exception("Token required")
 103
 104        else:
 105            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 106            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 107
 108        if accountId is None or not accountId:
 109            try:
 110                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 111                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 112
 113            except KeyError:
 114                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 115
 116        else:
 117            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 118            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 119
 120        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 121        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 122
 123        Latest version: https://pypi.org/project/tksbrokerapi/
 124        """
 125
 126        self.aliases = TKS_TICKER_ALIASES
 127        """Some aliases instead official tickers.
 128
 129        See also: `TKSEnums.TKS_TICKER_ALIASES`
 130        """
 131
 132        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 133
 134        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 135
 136        self.ticker = ""
 137        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 138
 139        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 140        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 141
 142        See also: `SearchByTicker()`, `SearchInstruments()`.
 143        """
 144
 145        self.figi = ""
 146        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 147
 148        See also: `SearchByFIGI()`, `SearchInstruments()`.
 149        """
 150
 151        self.depth = 1
 152        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 153
 154        See also: `GetCurrentPrices()`.
 155        """
 156
 157        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 158        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 159
 160        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 161        """
 162
 163        uLogger.debug("Broker API server: {}".format(self.server))
 164
 165        self.timeout = 15
 166        """Server operations timeout in seconds. Default: `15`.
 167
 168        See also: `SendAPIRequest()`.
 169        """
 170
 171        self.headers = {
 172            "Content-Type": "application/json",
 173            "accept": "application/json",
 174            "Authorization": "Bearer {}".format(self.token),
 175            "x-app-name": "Tim55667757.TKSBrokerAPI",
 176        }
 177        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 178
 179        See also: `SendAPIRequest()`.
 180        """
 181
 182        self.body = None
 183        """Request body which send to broker server. Default: `None`.
 184
 185        See also: `SendAPIRequest()`.
 186        """
 187
 188        self.moreDebug = False
 189        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 190
 191        self.historyFile = None
 192        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 193
 194        See also: `History()`.
 195        """
 196
 197        self.htmlHistoryFile = "index.html"
 198        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 199
 200        See also: `ShowHistoryChart()`.
 201        """
 202
 203        self.instrumentsFile = "instruments.md"
 204        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 205
 206        See also: `ShowInstrumentsInfo()`.
 207        """
 208
 209        self.searchResultsFile = "search-results.md"
 210        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 211
 212        See also: `SearchInstruments()`.
 213        """
 214
 215        self.pricesFile = "prices.md"
 216        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 217
 218        See also: `GetListOfPrices()`.
 219        """
 220
 221        self.infoFile = "info.md"
 222        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 223
 224        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 225        """
 226
 227        self.bondsXLSXFile = "ext-bonds.xlsx"
 228        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 229        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 230
 231        See also: `ExtendBondsData()`.
 232        """
 233
 234        self.calendarFile = "calendar.md"
 235        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 236        
 237        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 238
 239        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 240        """
 241
 242        self.overviewFile = "overview.md"
 243        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 244
 245        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 246        """
 247
 248        self.overviewDigestFile = "overview-digest.md"
 249        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 250
 251        See also: `Overview()` with parameter `details="digest"`.
 252        """
 253
 254        self.overviewPositionsFile = "overview-positions.md"
 255        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 256
 257        See also: `Overview()` with parameter `details="positions"`.
 258        """
 259
 260        self.overviewOrdersFile = "overview-orders.md"
 261        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 262
 263        See also: `Overview()` with parameter `details="orders"`.
 264        """
 265
 266        self.overviewAnalyticsFile = "overview-analytics.md"
 267        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 268
 269        See also: `Overview()` with parameter `details="analytics"`.
 270        """
 271
 272        self.overviewBondsCalendarFile = "overview-calendar.md"
 273        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 274
 275        See also: `Overview()` with parameter `details="calendar"`.
 276        """
 277
 278        self.reportFile = "deals.md"
 279        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 280
 281        See also: `Deals()`.
 282        """
 283
 284        self.withdrawalLimitsFile = "limits.md"
 285        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 286
 287        See also: `OverviewLimits()` and `RequestLimits()`.
 288        """
 289
 290        self.userInfoFile = "user-info.md"
 291        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 292
 293        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 294        """
 295
 296        self.userAccountsFile = "accounts.md"
 297        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 298
 299        See also: `OverviewAccounts()`, `RequestAccounts()`.
 300        """
 301
 302        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 303        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 304
 305        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 306
 307        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 308        """
 309
 310        self.iList = None  # init iList for raw instruments data
 311        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 312        
 313        See also: `Listing()`, `DumpInstruments()`.
 314        """
 315
 316        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 317        if useCache:
 318            if os.path.exists(self.iListDumpFile):
 319                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 320                curTime = datetime.now(tzutc())
 321
 322                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 323                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 324
 325                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 326
 327                else:
 328                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 329
 330                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 331                        os.path.abspath(self.iListDumpFile),
 332                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 333                    ))
 334
 335            else:
 336                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 337                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 338
 339        else:
 340            self.iList = self.Listing()  # request new raw instruments data from broker server
 341            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 342
 343        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 344        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 345
 346        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 347        """
 348
 349    def _ParseJSON(self, rawData="{}") -> dict:
 350        """
 351        Parse JSON from response string.
 352
 353        :param rawData: this is a string with JSON-formatted text.
 354        :return: JSON (dictionary), parsed from server response string.
 355        """
 356        responseJSON = json.loads(rawData) if rawData else {}
 357
 358        if self.moreDebug:
 359            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 360
 361        return responseJSON
 362
 363    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 364        """
 365        Send GET or POST request to broker server and receive JSON object.
 366
 367        self.header: must be defining with dictionary of headers.
 368        self.body: if define then used as request body. None by default.
 369        self.timeout: global request timeout, 15 seconds by default.
 370        :param url: url with REST request.
 371        :param reqType: send "GET" or "POST" request. "GET" by default.
 372        :param retry: how many times retry after first request if an 5xx server errors occurred.
 373        :param pause: sleep time in seconds between retries.
 374        :return: response JSON (dictionary) from broker.
 375        """
 376        if reqType.upper() not in ("GET", "POST"):
 377            uLogger.error("You can define request type: `GET` or `POST`!")
 378            raise Exception("Incorrect value")
 379
 380        if self.moreDebug:
 381            uLogger.debug("Request parameters:")
 382            uLogger.debug("    - REST API URL: {}".format(url))
 383            uLogger.debug("    - request type: {}".format(reqType))
 384            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 385            uLogger.debug("    - body:\n{}".format(self.body))
 386
 387        # fast hack to avoid all operations with some tickers/FIGI
 388        responseJSON = {}
 389        oK = True
 390        for item in self.exclude:
 391            if item in url:
 392                if self.moreDebug:
 393                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 394
 395                oK = False
 396                break
 397
 398        if oK:
 399            counter = 0
 400            response = None
 401            errMsg = ""
 402
 403            while not response and counter <= retry:
 404                if reqType == "GET":
 405                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 406
 407                if reqType == "POST":
 408                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 409
 410                if self.moreDebug:
 411                    uLogger.debug("Response:")
 412                    uLogger.debug("    - status code: {}".format(response.status_code))
 413                    uLogger.debug("    - reason: {}".format(response.reason))
 414                    uLogger.debug("    - body length: {}".format(len(response.text)))
 415                    uLogger.debug("    - headers:\n{}".format(response.headers))
 416
 417                # Server returns some headers:
 418                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 419                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 420                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 421                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 422                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 423                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 424                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 425                    sleep(rateLimitWait)
 426
 427                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 428                if 400 <= response.status_code < 500:
 429                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 430                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 431
 432                    if "code" in response.text and "message" in response.text:
 433                        msgDict = self._ParseJSON(rawData=response.text)
 434                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 435
 436                    counter = retry + 1  # do not retry for 4xx errors
 437
 438                if 500 <= response.status_code < 600:
 439                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 440                    uLogger.debug("    - not oK, {}".format(errMsg))
 441
 442                    if "code" in response.text and "message" in response.text:
 443                        errMsgDict = self._ParseJSON(rawData=response.text)
 444                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 445
 446                    counter += 1
 447
 448                    if counter <= retry:
 449                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 450                        sleep(pause)
 451
 452            responseJSON = self._ParseJSON(rawData=response.text)
 453
 454            if errMsg:
 455                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 456                uLogger.error("    - not oK, {}".format(errMsg))
 457
 458        return responseJSON
 459
 460    def _IUpdater(self, iType: str) -> tuple:
 461        """
 462        Request instrument by type from server. See available API methods for instruments:
 463        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 464        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 465        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 466        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 467        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 468
 469        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 470        :return: tuple with iType name and list of available instruments of current type for defined user token.
 471        """
 472        result = []
 473
 474        if iType in TKS_INSTRUMENTS:
 475            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 476
 477            # all instruments have the same body in API v2 requests:
 478            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 479            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 480            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 481
 482        return iType, result
 483
 484    def _IWrapper(self, kwargs):
 485        """
 486        Wrapper runs instrument's update method `_IUpdater()`.
 487        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 488        """
 489        return self._IUpdater(**kwargs)
 490
 491    def Listing(self) -> dict:
 492        """
 493        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 494
 495        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 496        """
 497        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 498        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 499
 500        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 501        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 502        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 503
 504        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 505        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 506        poolUpdater.close()
 507
 508        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 509        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 510        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 511
 512        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 513        for iType in iList.keys():
 514            for ticker in iList[iType]:
 515                iList[iType][ticker]["type"] = iType
 516
 517                if "minPriceIncrement" in iList[iType][ticker].keys():
 518                    iList[iType][ticker]["step"] = NanoToFloat(
 519                        iList[iType][ticker]["minPriceIncrement"]["units"],
 520                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 521                    )
 522
 523                else:
 524                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 525
 526        return iList
 527
 528    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 529        """
 530        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 531
 532        See also: `DumpInstruments()`, `Listing()`.
 533
 534        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 535                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 536        """
 537        if self.iListDumpFile is None or not self.iListDumpFile:
 538            uLogger.error("Output name of dump file must be defined!")
 539            raise Exception("Filename required")
 540
 541        if not self.iList or forceUpdate:
 542            self.iList = self.Listing()
 543
 544        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 545
 546        # Save as XLSX with separated sheets for every type of instruments:
 547        with pd.ExcelWriter(
 548                path=xlsxDumpFile,
 549                date_format=TKS_DATE_FORMAT,
 550                datetime_format=TKS_DATE_TIME_FORMAT,
 551                mode="w",
 552        ) as writer:
 553            for iType in TKS_INSTRUMENTS:
 554                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 555                df = df[sorted(df)]  # sorted by column names
 556                df = df.applymap(
 557                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 558                    na_action="ignore",
 559                )  # converting numbers from nano-type to float in every cell
 560                df.to_excel(
 561                    writer,
 562                    sheet_name=iType,
 563                    encoding="UTF-8",
 564                    freeze_panes=(1, 1),
 565                )  # saving as XLSX-file with freeze first row and column as headers
 566
 567        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 568
 569    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 570        """
 571        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 572        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 573
 574        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 575
 576        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 577                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 578        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 579        """
 580        if self.iListDumpFile is None or not self.iListDumpFile:
 581            uLogger.error("Output name of dump file must be defined!")
 582            raise Exception("Filename required")
 583
 584        if not self.iList or forceUpdate:
 585            self.iList = self.Listing()
 586
 587        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 588        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 589            fH.write(jsonDump)
 590
 591        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 592
 593        return jsonDump
 594
 595    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 596        """
 597        Show information about one instrument defined by json data and prints it in Markdown format.
 598
 599        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 600
 601        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 602        :param show: if `True` then also printing information about instrument and its current price.
 603        :return: multilines text in Markdown format with information about one instrument.
 604        """
 605        splitLine = "|                                                             |                                                        |\n"
 606        infoText = ""
 607
 608        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 609            info = [
 610                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 611                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 612                "| Parameters                                                  | Values                                                 |\n",
 613                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 614                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 615                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 616            ]
 617
 618            if "sector" in iJSON.keys() and iJSON["sector"]:
 619                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 620
 621            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 622                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 623
 624            info.extend([
 625                splitLine,
 626                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 627                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 628            ])
 629
 630            if "isin" in iJSON.keys() and iJSON["isin"]:
 631                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 632
 633            if "classCode" in iJSON.keys():
 634                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 635
 636            info.extend([
 637                splitLine,
 638                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 639                splitLine,
 640                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 641                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 642                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 643            ])
 644
 645            if iJSON["figi"]:
 646                self.figi = iJSON["figi"]
 647                iJSON = iJSON | self.RequestTradingStatus()
 648
 649                info.extend([
 650                    splitLine,
 651                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 652                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 653                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 654                ])
 655
 656            info.append(splitLine)
 657
 658            if "type" in iJSON.keys() and iJSON["type"]:
 659                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 660
 661                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 662                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 663
 664            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 665                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 666
 667            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 668                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 669
 670            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 671                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 672
 673            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 674                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 675
 676            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 677                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 678
 679            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 680                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 681
 682            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 683                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 684
 685            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 686                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 687
 688            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 689                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 690
 691            if "currency" in iJSON.keys():
 692                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 693
 694            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 695                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 696
 697            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 698                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 699
 700            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 701                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 702
 703            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 704                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 705
 706            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 707                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 708
 709            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 710                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 711
 712            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 713                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 714
 715            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 716                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 717
 718            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 719                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 720
 721            iExt = None
 722            if iJSON["type"] == "Bonds":
 723                info.extend([
 724                    splitLine,
 725                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 726                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 727                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 728                        iJSON["nominal"]["currency"],
 729                    )),
 730                ])
 731
 732                if "floatingCouponFlag" in iJSON.keys():
 733                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 734
 735                if "amortizationFlag" in iJSON.keys():
 736                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 737
 738                info.append(splitLine)
 739
 740                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 741                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 742
 743                if iJSON["figi"]:
 744                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 745
 746                    info.extend([
 747                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 748                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 749                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 750                    ])
 751
 752                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 753                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 754                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 755                        iJSON["aciValue"]["currency"]
 756                    )))
 757
 758            if "currentPrice" in iJSON.keys():
 759                info.append(splitLine)
 760
 761                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 762                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 763
 764                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 765                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 766                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 767                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 768                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 769
 770                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 771                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 772
 773                info.extend([
 774                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 775                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 776                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 777                    )),
 778                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 779                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 780                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 781                    )),
 782                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 783                        "{:.2f}%{}".format(
 784                            iJSON["currentPrice"]["changes"],
 785                            " ({}{:.2f} {})".format(
 786                                "+" if bondChangesDelta > 0 else "",
 787                                bondChangesDelta,
 788                                aciCurrency
 789                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 790                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 791                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 792                                currency
 793                            ),
 794                        )
 795                    ),
 796                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 797                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 798                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 799                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 800                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 801                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 802                    )),
 803                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 804                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 805                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 806                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 807                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 808                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 809                    )),
 810                ])
 811
 812            if "lot" in iJSON.keys():
 813                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 814
 815            if "step" in iJSON.keys() and iJSON["step"] != 0:
 816                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 817
 818            # Add bond payment calendar:
 819            if iJSON["type"] == "Bonds":
 820                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 821                info.extend(["\n", strCalendar])
 822
 823            infoText += "".join(info)
 824
 825            if show:
 826                uLogger.info("{}".format(infoText))
 827
 828            else:
 829                uLogger.debug("{}".format(infoText))
 830
 831            if self.infoFile is not None:
 832                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 833                    fH.write(infoText)
 834
 835                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 836
 837        return infoText
 838
 839    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 840        """
 841        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 842
 843        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 844        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 845        :return: JSON formatted data with information about instrument.
 846        """
 847        tickerJSON = {}
 848        if self.moreDebug:
 849            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 850
 851        if not self.ticker:
 852            uLogger.warning("self.ticker variable is not be empty!")
 853
 854        else:
 855            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 856                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 857                raise Exception("Instrument not allowed")
 858
 859            if not self.iList:
 860                self.iList = self.Listing()
 861
 862            if self.ticker in self.iList["Shares"].keys():
 863                tickerJSON = self.iList["Shares"][self.ticker]
 864                if self.moreDebug:
 865                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 866
 867            elif self.ticker in self.iList["Currencies"].keys():
 868                tickerJSON = self.iList["Currencies"][self.ticker]
 869                if self.moreDebug:
 870                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 871
 872            elif self.ticker in self.iList["Bonds"].keys():
 873                tickerJSON = self.iList["Bonds"][self.ticker]
 874                if self.moreDebug:
 875                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 876
 877            elif self.ticker in self.iList["Etfs"].keys():
 878                tickerJSON = self.iList["Etfs"][self.ticker]
 879                if self.moreDebug:
 880                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 881
 882            elif self.ticker in self.iList["Futures"].keys():
 883                tickerJSON = self.iList["Futures"][self.ticker]
 884                if self.moreDebug:
 885                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 886
 887        if tickerJSON:
 888            self.figi = tickerJSON["figi"]
 889
 890            if requestPrice:
 891                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 892
 893                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 894                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 895
 896                else:
 897                    tickerJSON["currentPrice"]["changes"] = 0
 898
 899            if show:
 900                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 901
 902        else:
 903            if show:
 904                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 905
 906        return tickerJSON
 907
 908    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 909        """
 910        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 911
 912        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 913        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 914        :return: JSON formatted data with information about instrument.
 915        """
 916        figiJSON = {}
 917        if self.moreDebug:
 918            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 919
 920        if not self.figi:
 921            uLogger.warning("self.figi variable is not be empty!")
 922
 923        else:
 924            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 925                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 926                raise Exception("Instrument not allowed")
 927
 928            if not self.iList:
 929                self.iList = self.Listing()
 930
 931            for item in self.iList["Shares"].keys():
 932                if self.figi == self.iList["Shares"][item]["figi"]:
 933                    figiJSON = self.iList["Shares"][item]
 934
 935                    if self.moreDebug:
 936                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 937
 938                    break
 939
 940            if not figiJSON:
 941                for item in self.iList["Currencies"].keys():
 942                    if self.figi == self.iList["Currencies"][item]["figi"]:
 943                        figiJSON = self.iList["Currencies"][item]
 944
 945                        if self.moreDebug:
 946                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 947
 948                        break
 949
 950            if not figiJSON:
 951                for item in self.iList["Bonds"].keys():
 952                    if self.figi == self.iList["Bonds"][item]["figi"]:
 953                        figiJSON = self.iList["Bonds"][item]
 954
 955                        if self.moreDebug:
 956                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 957
 958                        break
 959
 960            if not figiJSON:
 961                for item in self.iList["Etfs"].keys():
 962                    if self.figi == self.iList["Etfs"][item]["figi"]:
 963                        figiJSON = self.iList["Etfs"][item]
 964
 965                        if self.moreDebug:
 966                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 967
 968                        break
 969
 970            if not figiJSON:
 971                for item in self.iList["Futures"].keys():
 972                    if self.figi == self.iList["Futures"][item]["figi"]:
 973                        figiJSON = self.iList["Futures"][item]
 974
 975                        if self.moreDebug:
 976                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 977
 978                        break
 979
 980        if figiJSON:
 981            self.figi = figiJSON["figi"]
 982            self.ticker = figiJSON["ticker"]
 983
 984            if requestPrice:
 985                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 986
 987                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 988                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 989
 990                else:
 991                    figiJSON["currentPrice"]["changes"] = 0
 992
 993            if show:
 994                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 995
 996        else:
 997            if show:
 998                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
 999
1000        return figiJSON
1001
1002    def GetCurrentPrices(self, show: bool = True) -> dict:
1003        """
1004        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1005        `{"buy": [{"price": 1243.8, "quantity": 193},
1006                  {"price": 1244.0, "quantity": 168},
1007                  {"price": 1244.8, "quantity": 5},
1008                  {"price": 1245.0, "quantity": 61},
1009                  {"price": 1245.4, "quantity": 60}],
1010          "sell": [{"price": 1243.6, "quantity": 8},
1011                   {"price": 1242.6, "quantity": 10},
1012                   {"price": 1242.4, "quantity": 18},
1013                   {"price": 1242.2, "quantity": 50},
1014                   {"price": 1242.0, "quantity": 113}],
1015          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1016        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1017        - sell: list of dicts with Buyers prices,
1018            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1019            - quantity: volume value by current price in lots,
1020        - limitUp: current trade session limit price, maximum,
1021        - limitDown: current trade session limit price, minimum,
1022        - lastPrice: last deal price of the instrument,
1023        - closePrice: previous trade session close price of the instrument.
1024
1025        See also: `SearchByTicker()` and `SearchByFIGI()`.
1026        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1027        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1028
1029        :param show: if `True` then print DOM to log and console.
1030        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1031                 If an error occurred then returns an empty record:
1032                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1033        """
1034        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1035
1036        if self.depth < 1:
1037            uLogger.error("Depth of Market (DOM) must be >=1!")
1038            raise Exception("Incorrect value")
1039
1040        if not (self.ticker or self.figi):
1041            uLogger.error("self.ticker or self.figi variables must be defined!")
1042            raise Exception("Ticker or FIGI required")
1043
1044        if self.ticker and not self.figi:
1045            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1046            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1047
1048        if not self.ticker and self.figi:
1049            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1050            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1051
1052        if not self.figi:
1053            uLogger.error("FIGI is not defined!")
1054            raise Exception("Ticker or FIGI required")
1055
1056        else:
1057            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1058
1059            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1060            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1061            self.body = str({"figi": self.figi, "depth": self.depth})
1062            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1063
1064            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1065                # list of dicts with sellers orders:
1066                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1067
1068                # list of dicts with buyers orders:
1069                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1070
1071                # max price of instrument at this time:
1072                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1073
1074                # min price of instrument at this time:
1075                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1076
1077                # last price of deal with instrument:
1078                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1079
1080                # last close price of instrument:
1081                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1082
1083            else:
1084                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1085                uLogger.debug("Server response: {}".format(pricesResponse))
1086
1087            if show:
1088                if prices["buy"] or prices["sell"]:
1089                    info = [
1090                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1091                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1092                            self.ticker,
1093                            self.figi,
1094                            self.depth,
1095                        ),
1096                        "-" * 60, "\n",
1097                        "             Orders of Buyers | Orders of Sellers\n",
1098                        "-" * 60, "\n",
1099                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1100                        "-" * 60, "\n",
1101                    ]
1102
1103                    if not prices["buy"]:
1104                        info.append("                              | No orders!\n")
1105                        sumBuy = 0
1106
1107                    else:
1108                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1109                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1110                        for item in maxMinSorted:
1111                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1112
1113                    if not prices["sell"]:
1114                        info.append("No orders!                    |\n")
1115                        sumSell = 0
1116
1117                    else:
1118                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1119                        for item in prices["sell"]:
1120                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1121
1122                    info.extend([
1123                        "-" * 60, "\n",
1124                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1125                        "-" * 60, "\n",
1126                    ])
1127
1128                    infoText = "".join(info)
1129
1130                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1131
1132                else:
1133                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1134
1135        return prices
1136
1137    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1138        """
1139        This method get and show information about all available broker instruments for current user account.
1140        If `instrumentsFile` string is not empty then also save information to this file.
1141
1142        :param show: if `True` then print results to console, if `False` — print only to file.
1143        :return: multi-lines string with all available broker instruments
1144        """
1145        if not self.iList:
1146            self.iList = self.Listing()
1147
1148        info = [
1149            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1150            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1151        ]
1152
1153        # add instruments count by type:
1154        for iType in self.iList.keys():
1155            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1156
1157        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1158        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1159
1160        # generating info tables with all instruments by type:
1161        for iType in self.iList.keys():
1162            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1163
1164            for instrument in self.iList[iType].keys():
1165                iName = self.iList[iType][instrument]["name"]  # instrument's name
1166                if len(iName) > 57:
1167                    iName = "{}...".format(iName[:54])  # right trim for a long string
1168
1169                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1170                    self.iList[iType][instrument]["ticker"],
1171                    iName,
1172                    self.iList[iType][instrument]["figi"],
1173                    self.iList[iType][instrument]["currency"],
1174                    self.iList[iType][instrument]["lot"],
1175                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1176                ))
1177
1178        infoText = "".join(info)
1179
1180        if show:
1181            uLogger.info(infoText)
1182
1183        if self.instrumentsFile:
1184            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1185                fH.write(infoText)
1186
1187            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1188
1189        return infoText
1190
1191    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1192        """
1193        This method search and show information about instruments by part of its ticker, FIGI or name.
1194        If `searchResultsFile` string is not empty then also save information to this file.
1195
1196        :param pattern: string with part of ticker, FIGI or instrument's name.
1197        :param show: if `True` then print results to console, if `False` — return list of result only.
1198        :return: list of dictionaries with all found instruments.
1199        """
1200        if not self.iList:
1201            self.iList = self.Listing()
1202
1203        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1204        compiledPattern = re.compile(pattern, re.IGNORECASE)
1205
1206        for iType in self.iList:
1207            for instrument in self.iList[iType].values():
1208                searchResult = compiledPattern.search(" ".join(
1209                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1210                ))
1211
1212                if searchResult:
1213                    searchResults[iType][instrument["ticker"]] = instrument
1214
1215        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1216        info = [
1217            "# Search results\n\n",
1218            "* **Search pattern:** [{}]\n".format(pattern),
1219            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1220            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1221        ]
1222        infoShort = info[:]
1223
1224        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1225        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1226        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1227
1228        if resultsLen == 0:
1229            info.append("\nNo results\n")
1230            infoShort.append("\nNo results\n")
1231            uLogger.warning("No results. Try changing your search pattern.")
1232
1233        else:
1234            for iType in searchResults:
1235                iTypeValuesCount = len(searchResults[iType].values())
1236                if iTypeValuesCount > 0:
1237                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1238                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1239
1240                    for instrument in searchResults[iType].values():
1241                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1242                            instrument["type"],
1243                            instrument["ticker"],
1244                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1245                            instrument["figi"],
1246                        ))
1247
1248                    if iTypeValuesCount <= 5:
1249                        infoShort.extend(info[-iTypeValuesCount:])
1250
1251                    else:
1252                        infoShort.extend(info[-5:])
1253                        infoShort.append(skippedLine)
1254
1255        infoText = "".join(info)
1256        infoTextShort = "".join(infoShort)
1257
1258        if show:
1259            uLogger.info(infoTextShort)
1260            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1261
1262        if self.searchResultsFile:
1263            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1264                fH.write(infoText)
1265
1266            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1267
1268        return searchResults
1269
1270    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1271        """
1272        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1273
1274        :param instruments: list of strings with tickers or FIGIs.
1275        :return: list with unique instrument FIGIs only.
1276        """
1277        requestedInstruments = []
1278        for iName in instruments:
1279            if iName not in self.aliases.keys():
1280                if iName not in requestedInstruments:
1281                    requestedInstruments.append(iName)
1282
1283            else:
1284                if iName not in requestedInstruments:
1285                    if self.aliases[iName] not in requestedInstruments:
1286                        requestedInstruments.append(self.aliases[iName])
1287
1288        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1289
1290        onlyUniqueFIGIs = []
1291        for iName in requestedInstruments:
1292            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1293                continue
1294
1295            self.ticker = iName
1296            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1297
1298            if not iData:
1299                self.ticker = ""
1300                self.figi = iName
1301
1302                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1303
1304                if not iData:
1305                    self.figi = ""
1306                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1307
1308            if iData and iData["figi"] not in onlyUniqueFIGIs:
1309                onlyUniqueFIGIs.append(iData["figi"])
1310
1311        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1312
1313        return onlyUniqueFIGIs
1314
1315    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1316        """
1317        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1318
1319        See limits: https://tinkoff.github.io/investAPI/limits/
1320
1321        If `pricesFile` string is not empty then also save information to this file.
1322
1323        :param instruments: list of strings with tickers or FIGIs.
1324        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1325        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1326                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1327        """
1328        if instruments is None or not instruments:
1329            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1330            raise Exception("Ticker or FIGI required")
1331
1332        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1333
1334        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1335
1336        iList = []  # trying to get info and current prices about all unique instruments:
1337        for self.figi in onlyUniqueFIGIs:
1338            iData = self.SearchByFIGI(requestPrice=True)
1339            iList.append(iData)
1340
1341        self.ShowListOfPrices(iList, show)
1342
1343        return iList
1344
1345    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1346        """
1347        Show table contains current prices of given instruments.
1348
1349        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1350                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1351        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1352        :return: multilines text in Markdown format as a table contains current prices.
1353        """
1354        infoText = ""
1355
1356        if show or self.pricesFile:
1357            info = [
1358                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1359                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1360                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1361            ]
1362
1363            for item in iList:
1364                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1365                    item["ticker"],
1366                    item["figi"],
1367                    item["type"],
1368                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1369                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1370                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1371                    "{} / {}".format(
1372                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1373                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1374                    ),
1375                    "{} / {}".format(
1376                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1377                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1378                    ),
1379                    item["currency"],
1380                ))
1381
1382            infoText = "".join(info)
1383
1384            if show:
1385                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1386
1387            if self.pricesFile:
1388                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1389                    fH.write(infoText)
1390
1391                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1392
1393        return infoText
1394
1395    def RequestTradingStatus(self) -> dict:
1396        """
1397        Requesting trading status for the instrument defined by `figi` variable.
1398
1399        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1400
1401        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1402
1403        :return: dictionary with trading status attributes. Response example:
1404                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1405                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1406        """
1407        if self.figi is None or not self.figi:
1408            uLogger.error("Variable `figi` must be defined for using this method!")
1409            raise Exception("FIGI required")
1410
1411        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1412
1413        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1414        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1415        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1416
1417        if self.moreDebug:
1418            uLogger.debug("Records about current trading status successfully received")
1419
1420        return tradingStatus
1421
1422    def RequestPortfolio(self) -> dict:
1423        """
1424        Requesting actual user's portfolio for current `accountId`.
1425
1426        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1427
1428        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1429
1430        :return: dictionary with user's portfolio.
1431        """
1432        if self.accountId is None or not self.accountId:
1433            uLogger.error("Variable `accountId` must be defined for using this method!")
1434            raise Exception("Account ID required")
1435
1436        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1437
1438        self.body = str({"accountId": self.accountId})
1439        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1440        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1441
1442        if self.moreDebug:
1443            uLogger.debug("Records about user's portfolio successfully received")
1444
1445        return rawPortfolio
1446
1447    def RequestPositions(self) -> dict:
1448        """
1449        Requesting open positions by currencies and instruments for current `accountId`.
1450
1451        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1452
1453        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1454
1455        :return: dictionary with open positions by instruments.
1456        """
1457        if self.accountId is None or not self.accountId:
1458            uLogger.error("Variable `accountId` must be defined for using this method!")
1459            raise Exception("Account ID required")
1460
1461        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1462
1463        self.body = str({"accountId": self.accountId})
1464        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1465        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1466
1467        if self.moreDebug:
1468            uLogger.debug("Records about current open positions successfully received")
1469
1470        return rawPositions
1471
1472    def RequestPendingOrders(self) -> list:
1473        """
1474        Requesting current actual pending limit orders for current `accountId`.
1475
1476        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1477
1478        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1479
1480        :return: list of dictionaries with pending limit orders.
1481        """
1482        if self.accountId is None or not self.accountId:
1483            uLogger.error("Variable `accountId` must be defined for using this method!")
1484            raise Exception("Account ID required")
1485
1486        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1487
1488        self.body = str({"accountId": self.accountId})
1489        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1490        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1491
1492        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1493
1494        return rawOrders
1495
1496    def RequestStopOrders(self) -> list:
1497        """
1498        Requesting current actual stop orders for current `accountId`.
1499
1500        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1501
1502        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1503
1504        :return: list of dictionaries with stop orders.
1505        """
1506        if self.accountId is None or not self.accountId:
1507            uLogger.error("Variable `accountId` must be defined for using this method!")
1508            raise Exception("Account ID required")
1509
1510        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1511
1512        self.body = str({"accountId": self.accountId})
1513        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1514        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1515
1516        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1517
1518        return rawStopOrders
1519
1520    def Overview(self, show: bool = False, details: str = "full") -> dict:
1521        """
1522        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1523        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1524        and `overviewBondsCalendarFile` are defined then also save information to file.
1525
1526        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1527        many requests about the state of the portfolio, and then, based on the received data, a large number
1528        of calculation and statistics are collected.
1529
1530        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1531        :param details: how detailed should the information be?
1532        - `full` — shows full available information about portfolio status (by default),
1533        - `positions` — shows only open positions,
1534        - `orders` — shows only sections of open limits and stop orders.
1535        - `digest` — show a short digest of the portfolio status,
1536        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1537        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1538        :return: dictionary with client's raw portfolio and some statistics.
1539        """
1540        if self.accountId is None or not self.accountId:
1541            uLogger.error("Variable `accountId` must be defined for using this method!")
1542            raise Exception("Account ID required")
1543
1544        view = {
1545            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1546                "headers": {},  # list of dictionaries, response headers without "positions" section
1547                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1548                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1549                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1550                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1551                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1552                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1553                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1554                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1555                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1556            },
1557            "stat": {  # --- some statistics calculated using "raw" sections:
1558                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1559                "availableRUB": 0.,  # available rubles (without other currencies)
1560                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1561                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1562                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1563                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1564                "sharesCostRUB": 0.,  # costs of all shares in RUB
1565                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1566                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1567                "futuresCostRUB": 0.,  # costs of all futures in RUB
1568                "Currencies": [],  # list of dictionaries of all currencies statistics
1569                "Shares": [],  # list of dictionaries of all shares statistics
1570                "Bonds": [],  # list of dictionaries of all bonds statistics
1571                "Etfs": [],  # list of dictionaries of all etfs statistics
1572                "Futures": [],  # list of dictionaries of all futures statistics
1573                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1574                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1575                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1576                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1577                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1578            },
1579            "analytics": {  # --- some analytics of portfolio:
1580                "distrByAssets": {},  # portfolio distribution by assets
1581                "distrByCompanies": {},  # portfolio distribution by companies
1582                "distrBySectors": {},  # portfolio distribution by sectors
1583                "distrByCurrencies": {},  # portfolio distribution by currencies
1584                "distrByCountries": {},  # portfolio distribution by countries
1585                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1586            }
1587        }
1588
1589        details = details.lower()
1590        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1591        if details not in availableDetails:
1592            details = "full"
1593            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1594
1595        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1596
1597        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1598        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1599        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1600        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1601
1602        # save response headers without "positions" section:
1603        for key in portfolioResponse.keys():
1604            if key != "positions":
1605                view["raw"]["headers"][key] = portfolioResponse[key]
1606
1607            else:
1608                continue
1609
1610        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1611        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1612        for item in portfolioResponse["positions"]:
1613            if item["instrumentType"] == "currency":
1614                self.figi = item["figi"]
1615                curr = self.SearchByFIGI(requestPrice=False)
1616
1617                # current price of currency in RUB:
1618                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1619                    "name": curr["name"],
1620                    "currentPrice": NanoToFloat(
1621                        item["currentPrice"]["units"],
1622                        item["currentPrice"]["nano"]
1623                    ),
1624                }
1625
1626                view["raw"]["Currencies"].append(item)
1627
1628            elif item["instrumentType"] == "share":
1629                view["raw"]["Shares"].append(item)
1630
1631            elif item["instrumentType"] == "bond":
1632                view["raw"]["Bonds"].append(item)
1633
1634            elif item["instrumentType"] == "etf":
1635                view["raw"]["Etfs"].append(item)
1636
1637            elif item["instrumentType"] == "futures":
1638                view["raw"]["Futures"].append(item)
1639
1640            else:
1641                continue
1642
1643        # how many volume of currencies (by ISO currency name) are blocked:
1644        for item in view["raw"]["positions"]["blocked"]:
1645            blocked = NanoToFloat(item["units"], item["nano"])
1646            if blocked > 0:
1647                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1648
1649        # how many volume of instruments (by FIGI) are blocked:
1650        for item in view["raw"]["positions"]["securities"]:
1651            blocked = int(item["blocked"])
1652            if blocked > 0:
1653                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1654
1655        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1656
1657        if "rub" in allBlocked.keys():
1658            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1659
1660        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1661        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1662        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1663        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1664        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1665        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1666        view["stat"]["portfolioCostRUB"] = sum([
1667            view["stat"]["allCurrenciesCostRUB"],
1668            view["stat"]["sharesCostRUB"],
1669            view["stat"]["bondsCostRUB"],
1670            view["stat"]["etfsCostRUB"],
1671            view["stat"]["futuresCostRUB"],
1672        ])
1673
1674        # --- calculating some portfolio statistics:
1675        byComp = {}  # distribution by companies
1676        bySect = {}  # distribution by sectors
1677        byCurr = {}  # distribution by currencies (include RUB)
1678        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1679        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1680
1681        for item in portfolioResponse["positions"]:
1682            self.figi = item["figi"]
1683            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1684
1685            if instrument:
1686                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1687                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1688
1689                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1690                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1691
1692                else:
1693                    blocked = 0
1694
1695                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1696                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1697                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1698                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1699                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1700                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1701                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1702                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1703                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1704                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1705                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1706                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1707
1708                statData = {
1709                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1710                    "ticker": instrument["ticker"],  # ticker by FIGI
1711                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1712                    "volume": volume,  # available volume of instrument
1713                    "lots": lots,  # volume in lots of instrument
1714                    "direction": direction,  # direction of an instrument's position: short or long
1715                    "blocked": blocked,  # blocked volume of currency or instrument
1716                    "currentPrice": curPrice,  # current instrument's price in basic asset
1717                    "average": average,  # current average position price
1718                    "cost": cost,  # current cost of all volume of instrument in basic asset
1719                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1720                    "costRUB": costRUB,  # cost of instrument in ruble
1721                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1722                    "profit": profit,  # expected profit at current moment
1723                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1724                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1725                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1726                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1727                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1728                    "step": instrument["step"],  # minimum price increment
1729                }
1730
1731                # adding distribution by unique countries:
1732                if statData["country"] not in byCountry.keys():
1733                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1734
1735                else:
1736                    byCountry[statData["country"]]["cost"] += costRUB
1737                    byCountry[statData["country"]]["percent"] += percentCostRUB
1738
1739                if item["instrumentType"] != "currency":
1740                    # adding distribution by unique companies:
1741                    if statData["name"]:
1742                        if statData["name"] not in byComp.keys():
1743                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1744
1745                        else:
1746                            byComp[statData["name"]]["cost"] += costRUB
1747                            byComp[statData["name"]]["percent"] += percentCostRUB
1748
1749                    # adding distribution by unique sectors:
1750                    if statData["sector"] not in bySect.keys():
1751                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1752
1753                    else:
1754                        bySect[statData["sector"]]["cost"] += costRUB
1755                        bySect[statData["sector"]]["percent"] += percentCostRUB
1756
1757                # adding distribution by unique currencies:
1758                if currency not in byCurr.keys():
1759                    byCurr[currency] = {
1760                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1761                        "cost": costRUB,
1762                        "percent": percentCostRUB
1763                    }
1764
1765                else:
1766                    byCurr[currency]["cost"] += costRUB
1767                    byCurr[currency]["percent"] += percentCostRUB
1768
1769                # saving statistics for every instrument:
1770                if item["instrumentType"] == "currency":
1771                    view["stat"]["Currencies"].append(statData)
1772
1773                    # update dict with free funds for trading (total - blocked) by currencies
1774                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1775                    view["stat"]["funds"][currency] = {
1776                        "total": volume,
1777                        "totalCostRUB": costRUB,  # total volume cost in rubles
1778                        "free": volume - blocked,
1779                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1780                    }
1781
1782                elif item["instrumentType"] == "share":
1783                    view["stat"]["Shares"].append(statData)
1784
1785                elif item["instrumentType"] == "bond":
1786                    view["stat"]["Bonds"].append(statData)
1787
1788                elif item["instrumentType"] == "etf":
1789                    view["stat"]["Etfs"].append(statData)
1790
1791                elif item["instrumentType"] == "Futures":
1792                    view["stat"]["Futures"].append(statData)
1793
1794                else:
1795                    continue
1796
1797        # total changes in Russian Ruble:
1798        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1799        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1800        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1801        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1802        view["stat"]["funds"]["rub"] = {
1803            "total": view["stat"]["availableRUB"],
1804            "totalCostRUB": view["stat"]["availableRUB"],
1805            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1806            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1807        }
1808
1809        # --- pending limit orders sector data:
1810        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1811        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1812
1813        for item in view["raw"]["orders"]:
1814            self.figi = item["figi"]
1815
1816            if item["figi"] not in uniquePendingOrdersFIGIs:
1817                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1818
1819                uniquePendingOrdersFIGIs.append(item["figi"])
1820                uniquePendingOrders[item["figi"]] = instrument
1821
1822            else:
1823                instrument = uniquePendingOrders[item["figi"]]
1824
1825            if instrument:
1826                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1827                orderType = TKS_ORDER_TYPES[item["orderType"]]
1828                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1829                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1830
1831                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1832                if item["direction"] == "ORDER_DIRECTION_BUY":
1833                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1834
1835                else:
1836                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1837
1838                # requested price for order execution:
1839                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1840
1841                # necessary changes in percent to reach target from current price:
1842                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1843
1844                view["stat"]["orders"].append({
1845                    "orderID": item["orderId"],  # orderId number parameter of current order
1846                    "figi": item["figi"],  # FIGI identification
1847                    "ticker": instrument["ticker"],  # ticker name by FIGI
1848                    "lotsRequested": item["lotsRequested"],  # requested lots value
1849                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1850                    "currentPrice": lastPrice,  # current instrument's price for defined action
1851                    "targetPrice": target,  # requested price for order execution in base currency
1852                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1853                    "percentChanges": changes,  # changes in percent to target from current price
1854                    "currency": item["currency"],  # instrument's currency name
1855                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1856                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1857                    "status": orderState,  # order status from TKS_ORDER_STATES
1858                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1859                })
1860
1861        # --- stop orders sector data:
1862        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1863        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1864
1865        for item in view["raw"]["stopOrders"]:
1866            self.figi = item["figi"]
1867
1868            if item["figi"] not in uniqueStopOrdersFIGIs:
1869                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1870
1871                uniqueStopOrdersFIGIs.append(item["figi"])
1872                uniqueStopOrders[item["figi"]] = instrument
1873
1874            else:
1875                instrument = uniqueStopOrders[item["figi"]]
1876
1877            if instrument:
1878                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1879                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1880                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1881
1882                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1883                if "expirationTime" in item.keys():
1884                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1885                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1886
1887                else:
1888                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1889                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1890
1891                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1892                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1893                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1894
1895                else:
1896                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1897
1898                # requested price when stop-order executed:
1899                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1900
1901                # price for limit-order, set up when stop-order executed:
1902                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1903
1904                # necessary changes in percent to reach target from current price:
1905                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1906
1907                view["stat"]["stopOrders"].append({
1908                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1909                    "figi": item["figi"],  # FIGI identification
1910                    "ticker": instrument["ticker"],  # ticker name by FIGI
1911                    "lotsRequested": item["lotsRequested"],  # requested lots value
1912                    "currentPrice": lastPrice,  # current instrument's price for defined action
1913                    "targetPrice": target,  # requested price for stop-order execution in base currency
1914                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1915                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1916                    "percentChanges": changes,  # changes in percent to target from current price
1917                    "currency": item["currency"],  # instrument's currency name
1918                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1919                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1920                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1921                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1922                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1923                })
1924
1925        # --- calculating data for analytics section:
1926        # portfolio distribution by assets:
1927        view["analytics"]["distrByAssets"] = {
1928            "Ruble": {
1929                "uniques": 1,
1930                "cost": view["stat"]["availableRUB"],
1931                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1932            },
1933            "Currencies": {
1934                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1935                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1936                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1937            },
1938            "Shares": {
1939                "uniques": len(view["stat"]["Shares"]),
1940                "cost": view["stat"]["sharesCostRUB"],
1941                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1942            },
1943            "Bonds": {
1944                "uniques": len(view["stat"]["Bonds"]),
1945                "cost": view["stat"]["bondsCostRUB"],
1946                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1947            },
1948            "Etfs": {
1949                "uniques": len(view["stat"]["Etfs"]),
1950                "cost": view["stat"]["etfsCostRUB"],
1951                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1952            },
1953            "Futures": {
1954                "uniques": len(view["stat"]["Futures"]),
1955                "cost": view["stat"]["futuresCostRUB"],
1956                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1957            },
1958        }
1959
1960        # portfolio distribution by companies:
1961        view["analytics"]["distrByCompanies"]["All money cash"] = {
1962            "ticker": "",
1963            "cost": view["stat"]["allCurrenciesCostRUB"],
1964            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1965        }
1966        view["analytics"]["distrByCompanies"].update(byComp)
1967
1968        # portfolio distribution by sectors:
1969        view["analytics"]["distrBySectors"]["All money cash"] = {
1970            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1971            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1972        }
1973        view["analytics"]["distrBySectors"].update(bySect)
1974
1975        # portfolio distribution by currencies:
1976        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1977            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1978
1979            if self.moreDebug:
1980                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1981
1982        view["analytics"]["distrByCurrencies"].update(byCurr)
1983        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1984        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1985
1986        # portfolio distribution by countries:
1987        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1988            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1989
1990            if self.moreDebug:
1991                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1992
1993        view["analytics"]["distrByCountries"].update(byCountry)
1994        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1995        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1996
1997        # --- Prepare text statistics overview in human-readable:
1998        if show:
1999            # Whatever the value `details`, header not changes:
2000            info = [
2001                "# Client's portfolio\n\n",
2002                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2003                "* **Account ID:** [{}]\n".format(self.accountId),
2004            ]
2005
2006            if details in ["full", "positions", "digest"]:
2007                info.extend([
2008                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2009                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2010                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2011                        view["stat"]["totalChangesRUB"],
2012                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2013                        view["stat"]["totalChangesPercentRUB"],
2014                    ),
2015                ])
2016
2017            if details in ["full", "positions"]:
2018                info.extend([
2019                    "## Open positions\n\n",
2020                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2021                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2022                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2023                        "{:.2f} ({:.2f}) rub".format(
2024                            view["stat"]["availableRUB"],
2025                            view["stat"]["blockedRUB"],
2026                        )
2027                    )
2028                ])
2029
2030                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2031                    return [
2032                        "|                             |                                 |          |              |              |                     |                              |\n",
2033                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2034                            noTradeStr if noTradeStr else typeStr,
2035                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2036                        ),
2037                    ]
2038
2039                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2040                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2041                        "{} [{}]".format(data["ticker"], data["figi"]),
2042                        "{:.2f} ({:.2f}) {}".format(
2043                            data["volume"],
2044                            data["blocked"],
2045                            data["currency"],
2046                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2047                            data["volume"],
2048                            data["blocked"],
2049                        ),
2050                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2051                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2052                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2053                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2054                        "{}{:.2f} {} ({}{:.2f}%)".format(
2055                            "+" if data["profit"] > 0 else "",
2056                            data["profit"], data["baseCurrencyName"],
2057                            "+" if data["percentProfit"] > 0 else "",
2058                            data["percentProfit"],
2059                        ),
2060                    )
2061
2062                # --- Show currencies section:
2063                if view["stat"]["Currencies"]:
2064                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2065                    for item in view["stat"]["Currencies"]:
2066                        info.append(_InfoStr(item, showCurrencyName=True))
2067
2068                else:
2069                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2070
2071                # --- Show shares section:
2072                if view["stat"]["Shares"]:
2073                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2074
2075                    for item in view["stat"]["Shares"]:
2076                        info.append(_InfoStr(item))
2077
2078                else:
2079                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2080
2081                # --- Show bonds section:
2082                if view["stat"]["Bonds"]:
2083                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2084
2085                    for item in view["stat"]["Bonds"]:
2086                        info.append(_InfoStr(item))
2087
2088                else:
2089                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2090
2091                # --- Show etfs section:
2092                if view["stat"]["Etfs"]:
2093                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2094
2095                    for item in view["stat"]["Etfs"]:
2096                        info.append(_InfoStr(item))
2097
2098                else:
2099                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2100
2101                # --- Show futures section:
2102                if view["stat"]["Futures"]:
2103                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2104
2105                    for item in view["stat"]["Futures"]:
2106                        info.append(_InfoStr(item))
2107
2108                else:
2109                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2110
2111            if details in ["full", "orders"]:
2112                # --- Show pending limit orders section:
2113                if view["stat"]["orders"]:
2114                    info.extend([
2115                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2116                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2117                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2118                    ])
2119
2120                    for item in view["stat"]["orders"]:
2121                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2122                            "{} [{}]".format(item["ticker"], item["figi"]),
2123                            item["orderID"],
2124                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2125                            "{} {} ({}{:.2f}%)".format(
2126                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2127                                item["baseCurrencyName"],
2128                                "+" if item["percentChanges"] > 0 else "",
2129                                float(item["percentChanges"]),
2130                            ),
2131                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2132                            item["action"],
2133                            item["type"],
2134                            item["date"],
2135                        ))
2136
2137                else:
2138                    info.append("\n## Total pending limit-orders: 0\n")
2139
2140                # --- Show stop orders section:
2141                if view["stat"]["stopOrders"]:
2142                    info.extend([
2143                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2144                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2145                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2146                    ])
2147
2148                    for item in view["stat"]["stopOrders"]:
2149                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2150                            "{} [{}]".format(item["ticker"], item["figi"]),
2151                            item["orderID"],
2152                            item["lotsRequested"],
2153                            "{} {} ({}{:.2f}%)".format(
2154                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2155                                item["baseCurrencyName"],
2156                                "+" if item["percentChanges"] > 0 else "",
2157                                float(item["percentChanges"]),
2158                            ),
2159                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2160                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2161                            item["action"],
2162                            item["type"],
2163                            item["expType"],
2164                            item["createDate"],
2165                            item["expDate"],
2166                        ))
2167
2168                else:
2169                    info.append("\n## Total stop-orders: 0\n")
2170
2171            if details in ["full", "analytics"]:
2172                # -- Show analytics section:
2173                if view["stat"]["portfolioCostRUB"] > 0:
2174                    info.extend([
2175                        "\n# Analytics\n"
2176                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2177                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2178                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2179                            view["stat"]["totalChangesRUB"],
2180                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2181                            view["stat"]["totalChangesPercentRUB"],
2182                        ),
2183                        "\n## Portfolio distribution by assets\n"
2184                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2185                        "|------------------------------------|---------|---------|--------------------|\n",
2186                    ])
2187
2188                    for key in view["analytics"]["distrByAssets"].keys():
2189                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2190                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2191                                key,
2192                                view["analytics"]["distrByAssets"][key]["uniques"],
2193                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2194                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2195                            ))
2196
2197                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2198
2199                    info.extend([
2200                        "\n## Portfolio distribution by companies\n"
2201                        "\n| Company                                      | Percent | Current cost       |\n",
2202                        aSepLine,
2203                    ])
2204
2205                    for company in view["analytics"]["distrByCompanies"].keys():
2206                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2207                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2208                                "{}{}".format(
2209                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2210                                    company,
2211                                ),
2212                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2213                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2214                            ))
2215
2216                    info.extend([
2217                        "\n## Portfolio distribution by sectors\n"
2218                        "\n| Sector                                       | Percent | Current cost       |\n",
2219                        aSepLine,
2220                    ])
2221
2222                    for sector in view["analytics"]["distrBySectors"].keys():
2223                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2224                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2225                                sector,
2226                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2227                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2228                            ))
2229
2230                    info.extend([
2231                        "\n## Portfolio distribution by currencies\n"
2232                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2233                        aSepLine,
2234                    ])
2235
2236                    for curr in view["analytics"]["distrByCurrencies"].keys():
2237                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2238                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2239                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2240                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2241                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2242                            ))
2243
2244                    info.extend([
2245                        "\n## Portfolio distribution by countries\n"
2246                        "\n| Assets by country                            | Percent | Current cost       |\n",
2247                        aSepLine,
2248                    ])
2249
2250                    for country in view["analytics"]["distrByCountries"].keys():
2251                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2252                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2253                                country,
2254                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2255                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2256                            ))
2257
2258            if details in ["full", "calendar"]:
2259                # -- Show bonds payment calendar section:
2260                if view["stat"]["Bonds"]:
2261                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2262                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2263                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2264
2265                else:
2266                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2267
2268            infoText = "".join(info)
2269
2270            uLogger.info(infoText)
2271
2272            if details == "full" and self.overviewFile:
2273                filename = self.overviewFile
2274
2275            elif details == "digest" and self.overviewDigestFile:
2276                filename = self.overviewDigestFile
2277
2278            elif details == "positions" and self.overviewPositionsFile:
2279                filename = self.overviewPositionsFile
2280
2281            elif details == "orders" and self.overviewOrdersFile:
2282                filename = self.overviewOrdersFile
2283
2284            elif details == "analytics" and self.overviewAnalyticsFile:
2285                filename = self.overviewAnalyticsFile
2286
2287            elif details == "calendar" and self.overviewBondsCalendarFile:
2288                filename = self.overviewBondsCalendarFile
2289
2290            else:
2291                filename = ""
2292
2293            if filename:
2294                with open(filename, "w", encoding="UTF-8") as fH:
2295                    fH.write(infoText)
2296
2297                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2298
2299        return view
2300
2301    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2302        """
2303        Returns history operations between two given dates for current `accountId`.
2304        If `reportFile` string is not empty then also save human-readable report.
2305        Shows some statistical data of closed positions.
2306
2307        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2308        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2309        :param show: if `True` then also prints all records to the console.
2310        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2311        :return: original list of dictionaries with history of deals records from API ("operations" key):
2312                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2313                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2314        """
2315        if self.accountId is None or not self.accountId:
2316            uLogger.error("Variable `accountId` must be defined for using this method!")
2317            raise Exception("Account ID required")
2318
2319        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2320
2321        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2322
2323        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2324        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2325        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2326        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2327        customStat = {}  # custom statistics in additional to responseJSON
2328
2329        # --- output report in human-readable format:
2330        if show or self.reportFile:
2331            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2332            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2333            nextDay = ""
2334
2335            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2336
2337            if len(ops) > 0:
2338                customStat = {
2339                    "opsCount": 0,  # total operations count
2340                    "buyCount": 0,  # buy operations
2341                    "sellCount": 0,  # sell operations
2342                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2343                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2344                    "payIn": {"rub": 0.},  # Deposit brokerage account
2345                    "payOut": {"rub": 0.},  # Withdrawals
2346                    "divs": {"rub": 0.},  # Dividends income
2347                    "coupons": {"rub": 0.},  # Coupon's income
2348                    "brokerCom": {"rub": 0.},  # Service commissions
2349                    "serviceCom": {"rub": 0.},  # Service commissions
2350                    "marginCom": {"rub": 0.},  # Margin commissions
2351                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2352                }
2353
2354                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2355                for item in ops:
2356                    if item["state"] == "OPERATION_STATE_EXECUTED":
2357                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2358
2359                        # count buy operations:
2360                        if "_BUY" in item["operationType"]:
2361                            customStat["buyCount"] += 1
2362
2363                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2364                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2365
2366                            else:
2367                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2368
2369                        # count sell operations:
2370                        elif "_SELL" in item["operationType"]:
2371                            customStat["sellCount"] += 1
2372
2373                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2374                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2375
2376                            else:
2377                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2378
2379                        # count incoming operations:
2380                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2381                            if item["payment"]["currency"] in customStat["payIn"].keys():
2382                                customStat["payIn"][item["payment"]["currency"]] += payment
2383
2384                            else:
2385                                customStat["payIn"][item["payment"]["currency"]] = payment
2386
2387                        # count withdrawals operations:
2388                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2389                            if item["payment"]["currency"] in customStat["payOut"].keys():
2390                                customStat["payOut"][item["payment"]["currency"]] += payment
2391
2392                            else:
2393                                customStat["payOut"][item["payment"]["currency"]] = payment
2394
2395                        # count dividends income:
2396                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2397                            if item["payment"]["currency"] in customStat["divs"].keys():
2398                                customStat["divs"][item["payment"]["currency"]] += payment
2399
2400                            else:
2401                                customStat["divs"][item["payment"]["currency"]] = payment
2402
2403                        # count coupon's income:
2404                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2405                            if item["payment"]["currency"] in customStat["coupons"].keys():
2406                                customStat["coupons"][item["payment"]["currency"]] += payment
2407
2408                            else:
2409                                customStat["coupons"][item["payment"]["currency"]] = payment
2410
2411                        # count broker commissions:
2412                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2413                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2414                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2418
2419                        # count service commissions:
2420                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2421                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2422                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2423
2424                            else:
2425                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2426
2427                        # count margin commissions:
2428                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2429                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2430                                customStat["marginCom"][item["payment"]["currency"]] += payment
2431
2432                            else:
2433                                customStat["marginCom"][item["payment"]["currency"]] = payment
2434
2435                        # count withholding taxes:
2436                        elif "_TAX" in item["operationType"]:
2437                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2438                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2439
2440                            else:
2441                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2442
2443                        else:
2444                            continue
2445
2446                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2447
2448                # --- view "Actions" lines:
2449                info.extend([
2450                    "| Report sections            |                               |                              |                      |                        |\n",
2451                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2452                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2453                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2454                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2455                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2456                    ),
2457                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2458                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2459                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2460                    ),
2461                ])
2462
2463                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2464                for key in opsKeys:
2465                    if key == "rub":
2466                        continue
2467
2468                    info.extend([
2469                        "|                            |                               | {:<28} |                      |                        |\n".format(
2470                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2471                        ),
2472                        "|                            |                               | {:<28} |                      |                        |\n".format(
2473                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2474                        ),
2475                    ])
2476
2477                info.append(splitLine1)
2478
2479                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2480                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2481                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2482                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2483                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2484                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2485                    )
2486
2487                # --- view "Payments" lines:
2488                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2489                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2490
2491                for key in paymentsKeys:
2492                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2493
2494                info.append(splitLine1)
2495
2496                # --- view "Commissions and taxes" lines:
2497                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2498                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2499
2500                for key in comKeys:
2501                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2502
2503                info.append(splitLine1)
2504
2505                info.extend([
2506                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2507                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2508                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2509                ])
2510
2511            else:
2512                info.append("Broker returned no operations during this period\n")
2513
2514            # --- view "Operations" section:
2515            for item in ops:
2516                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2517                    continue
2518
2519                else:
2520                    self.figi = item["figi"] if item["figi"] else ""
2521                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2522                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2523
2524                    # group of deals during one day:
2525                    if nextDay and item["date"].split("T")[0] != nextDay:
2526                        info.append(splitLine2)
2527                        nextDay = ""
2528
2529                    else:
2530                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2531
2532                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2533                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2534                        self.figi if self.figi else "—",
2535                        instrument["ticker"] if instrument else "—",
2536                        instrument["type"] if instrument else "—",
2537                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2538                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2539                        TKS_OPERATION_STATES[item["state"]],
2540                        TKS_OPERATION_TYPES[item["operationType"]],
2541                    ))
2542
2543            infoText = "".join(info)
2544
2545            if show:
2546                if self.moreDebug:
2547                    uLogger.debug("Records about history of a client's operations successfully received")
2548
2549                uLogger.info(infoText)
2550
2551            if self.reportFile:
2552                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2553                    fH.write(infoText)
2554
2555                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2556
2557        return ops, customStat
2558
2559    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2560        """
2561        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2562
2563        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2564        Warning! Broker server used ISO UTC time by default.
2565
2566        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2567        Also, `historyFile` used to update history with `onlyMissing` parameter.
2568
2569        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2570
2571        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2572        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2573        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2574                         `"hour"`, `"day"`. Default: `"hour"`.
2575        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2576                            False by default. Warning! History appends only from last candle to current time
2577                            with always update last candle!
2578        :param csvSep: separator if csv-file is used, `,` by default.
2579        :param show: if `True` then also prints Pandas DataFrame to the console.
2580        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2581                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2582        """
2583        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2584        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2585        history = None  # empty pandas object for history
2586
2587        if interval not in TKS_CANDLE_INTERVALS.keys():
2588            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2589            raise Exception("Incorrect value")
2590
2591        if not (self.ticker or self.figi):
2592            uLogger.error("Ticker or FIGI must be defined!")
2593            raise Exception("Ticker or FIGI required")
2594
2595        if self.ticker and not self.figi:
2596            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2597            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2598
2599        if self.figi and not self.ticker:
2600            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2601            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2602
2603        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2604        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2605        if interval.lower() != "day":
2606            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2607
2608        delta = dtEnd - dtStart  # current UTC time minus last time in file
2609        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2610
2611        # calculate history length in candles:
2612        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2613        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2614            length += 1  # to avoid fraction time
2615
2616        # calculate data blocks count:
2617        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2618
2619        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2620        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2621        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2622        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2623        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2624
2625        tempOld = None  # pandas object for old history, if --only-missing key present
2626        lastTime = None  # datetime object of last old candle in file
2627
2628        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2629            uLogger.debug("--only-missing key present, add only last missing candles...")
2630            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2631
2632            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2633
2634            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2635            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2636            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2637            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2638
2639            # get last datetime object from last string in file or minus 1 delta if file is empty:
2640            if len(tempOld) > 0:
2641                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2642
2643            else:
2644                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2645
2646            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2647
2648        responseJSONs = []  # raw history blocks of data
2649
2650        blockEnd = dtEnd
2651        for item in range(blocks):
2652            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2653            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2654
2655            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2656                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2657            ))
2658
2659            if blockStart == blockEnd:
2660                uLogger.debug("Skipped this zero-length block...")
2661
2662            else:
2663                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2664                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2665                self.body = str({
2666                    "figi": self.figi,
2667                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2668                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2669                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2670                })
2671                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2672
2673                if "code" in responseJSON.keys():
2674                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2675
2676                else:
2677                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2678                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2679
2680                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2681
2682            blockEnd = blockStart
2683
2684        printCount = len(responseJSONs)  # candles to show in console
2685        if responseJSONs:
2686            tempHistory = pd.DataFrame(
2687                data={
2688                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2689                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2690                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2691                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2692                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2693                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2694                    "volume": [int(item["volume"]) for item in responseJSONs],
2695                },
2696                index=range(len(responseJSONs)),
2697                columns=["date", "time", "open", "high", "low", "close", "volume"],
2698            )
2699            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2700            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2701
2702            # append only newest candles to old history if --only-missing key present:
2703            if onlyMissing and tempOld is not None and lastTime is not None:
2704                index = 0  # find start index in tempHistory data:
2705
2706                for i, item in tempHistory.iterrows():
2707                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2708
2709                    if curTime == lastTime:
2710                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2711                        index = i
2712                        printCount = index + 1
2713                        break
2714
2715                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2716
2717            else:
2718                history = tempHistory  # if no `--only-missing` key then load full data from server
2719
2720            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2721
2722        if history is not None and not history.empty:
2723            if show:
2724                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2725                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2726                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2727                ))
2728
2729        else:
2730            uLogger.warning("Received an empty candles history!")
2731
2732        if self.historyFile is not None:
2733            if history is not None and not history.empty:
2734                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2735                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2736
2737            else:
2738                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2739
2740        else:
2741            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2742
2743        return history
2744
2745    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2746        """
2747        Load candles history from csv-file and return Pandas DataFrame object.
2748
2749        See also: `History()` and `ShowHistoryChart()` methods.
2750
2751        :param filePath: path to csv-file to open.
2752        """
2753        loadedHistory = None  # init candles data object
2754
2755        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2756
2757        if os.path.exists(filePath):
2758            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2759
2760            tfStr = self.priceModel.FormattedDelta(
2761                self.priceModel.timeframe,
2762                "{days} days {hours}h {minutes}m {seconds}s",
2763            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2764                self.priceModel.timeframe,
2765                "{hours}h {minutes}m {seconds}s",
2766            )
2767
2768            if loadedHistory is not None and not loadedHistory.empty:
2769                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2770                    len(loadedHistory),
2771                    tfStr,
2772                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2773                )
2774
2775            else:
2776                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2777
2778        else:
2779            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2780
2781        return loadedHistory
2782
2783    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2784        """
2785        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2786
2787        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2788        Default: `index.html` (both for interact and non-interact candlesticks chart).
2789
2790        See also: `History()` and `LoadHistory()` methods.
2791
2792        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2793        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2794                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2795                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2796                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2797        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2798                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2799        """
2800        if isinstance(candles, str):
2801            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2802            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2803
2804        elif isinstance(candles, pd.DataFrame):
2805            self.priceModel.prices = candles  # set candles chain from variable
2806            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2807
2808            if "datetime" not in candles.columns:
2809                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2810
2811        else:
2812            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2813            raise Exception("Incorrect value")
2814
2815        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2816
2817        if interact:
2818            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2819
2820            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2821
2822        else:
2823            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2824
2825            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2826
2827        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2828
2829    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2830        """
2831        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2832        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2833
2834        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2835
2836        :param operation: string "Buy" or "Sell".
2837        :param lots: volume, integer count of lots >= 1.
2838        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2839        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2840        :param expDate: string "Undefined" by default or local date in future,
2841                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2842        :return: JSON with response from broker server.
2843        """
2844        if self.accountId is None or not self.accountId:
2845            uLogger.error("Variable `accountId` must be defined for using this method!")
2846            raise Exception("Account ID required")
2847
2848        if operation is None or not operation or operation not in ("Buy", "Sell"):
2849            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2850            raise Exception("Incorrect value")
2851
2852        if lots is None or lots < 1:
2853            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2854            lots = 1
2855
2856        if tp is None or tp < 0:
2857            tp = 0
2858
2859        if sl is None or sl < 0:
2860            sl = 0
2861
2862        if expDate is None or not expDate:
2863            expDate = "Undefined"
2864
2865        if not (self.ticker or self.figi):
2866            uLogger.error("Ticker or FIGI must be defined!")
2867            raise Exception("Ticker or FIGI required")
2868
2869        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2870        self.ticker = instrument["ticker"]
2871        self.figi = instrument["figi"]
2872
2873        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2874
2875        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2876        self.body = str({
2877            "figi": self.figi,
2878            "quantity": str(lots),
2879            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2880            "accountId": str(self.accountId),
2881            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2882        })
2883        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2884
2885        if "orderId" in response.keys():
2886            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2887                operation, response["orderId"],
2888                self.ticker, self.figi, lots,
2889                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2890                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2891                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2892            ))
2893
2894            if tp > 0:
2895                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2896
2897            if sl > 0:
2898                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2899
2900        else:
2901            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2902
2903        return response
2904
2905    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2906        """
2907        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2908        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2909
2910        See also: `Order()` and `Trade()` docstrings.
2911
2912        :param lots: volume, integer count of lots >= 1.
2913        :param tp: float > 0, take profit price of stop-order.
2914        :param sl: float > 0, stop loss price of stop-order.
2915        :param expDate: it's a local date in future.
2916                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2917        :return: JSON with response from broker server.
2918        """
2919        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2920
2921    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2922        """
2923        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2924        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2925
2926        See also: `Order()` and `Trade()` docstrings.
2927
2928        :param lots: volume, integer count of lots >= 1.
2929        :param tp: float > 0, take profit price of stop-order.
2930        :param sl: float > 0, stop loss price of stop-order.
2931        :param expDate: it's a local date in the future.
2932                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2933        :return: JSON with response from broker server.
2934        """
2935        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2936
2937    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2938        """
2939        Close position of given instruments.
2940
2941        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2942        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2943                         This avoids unnecessary downloading data from the server.
2944        """
2945        if instruments is None or not instruments:
2946            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2947            raise Exception("Ticker or FIGI required")
2948
2949        if isinstance(instruments, str):
2950            instruments = [instruments]
2951
2952        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2953        if uniqueInstruments:
2954            if portfolio is None or not portfolio:
2955                portfolio = self.Overview(show=False)
2956
2957            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2958            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2959
2960            for self.figi in uniqueInstruments:
2961                if self.figi not in allOpened:
2962                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2963                    continue
2964
2965                # search open trade info about instrument by ticker:
2966                instrument = {}
2967                for iType in TKS_INSTRUMENTS:
2968                    if instrument:
2969                        break
2970
2971                    for item in portfolio["stat"][iType]:
2972                        if item["figi"] == self.figi:
2973                            instrument = item
2974                            break
2975
2976                if instrument:
2977                    self.ticker = instrument["ticker"]
2978                    self.figi = instrument["figi"]
2979
2980                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2981                        self.ticker,
2982                        self.figi,
2983                        int(instrument["volume"]),
2984                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2985                    ))
2986
2987                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2988
2989                    if tradeLots > 0:
2990                        if instrument["blocked"] > 0:
2991                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2992                                instrument["blocked"],
2993                                self.ticker,
2994                                tradeLots,
2995                            ))
2996
2997                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2998                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2999
3000                    else:
3001                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3002
3003    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3004        """
3005        Close all positions of given instruments with defined type.
3006
3007        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3008        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3009                         This avoids unnecessary downloading data from the server.
3010        """
3011        if iType not in TKS_INSTRUMENTS:
3012            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3013
3014        else:
3015            if portfolio is None or not portfolio:
3016                portfolio = self.Overview(show=False)
3017
3018            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3019            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3020
3021            if tickers and portfolio:
3022                self.CloseTrades(tickers, portfolio)
3023
3024            else:
3025                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3026
3027    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3028        """
3029        Universal method to create market or limit orders with all available parameters for current `accountId`.
3030        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3031
3032        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3033        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3034
3035        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3036        then broker immediately open market order as you can do simple --buy or --sell operations!
3037
3038        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3039        When current price will go up or down to target price value then broker opens a limit order.
3040        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3041
3042        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3043
3044        :param operation: string "Buy" or "Sell".
3045        :param orderType: string "Limit" or "Stop".
3046        :param lots: volume, integer count of lots >= 1.
3047        :param targetPrice: target price > 0. This is open trade price for limit order.
3048        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3049                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3050        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3051                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3052                         Stop loss order always executed by market price.
3053        :param expDate: string "Undefined" by default or local date in future.
3054                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3055                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3056                        A limit order has no expiration date, it lasts until the end of the trading day.
3057        :return: JSON with response from broker server.
3058        """
3059        if self.accountId is None or not self.accountId:
3060            uLogger.error("Variable `accountId` must be defined for using this method!")
3061            raise Exception("Account ID required")
3062
3063        if operation is None or not operation or operation not in ("Buy", "Sell"):
3064            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3065            raise Exception("Incorrect value")
3066
3067        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3068            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3069            raise Exception("Incorrect value")
3070
3071        if lots is None or lots < 1:
3072            uLogger.error("You must define trade volume > 0: integer count of lots!")
3073            raise Exception("Incorrect value")
3074
3075        if targetPrice is None or targetPrice <= 0:
3076            uLogger.error("Target price for limit-order must be greater than 0!")
3077            raise Exception("Incorrect value")
3078
3079        if limitPrice is None or limitPrice <= 0:
3080            limitPrice = targetPrice
3081
3082        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3083            stopType = "Limit"
3084
3085        if expDate is None or not expDate:
3086            expDate = "Undefined"
3087
3088        if not (self.ticker or self.figi):
3089            uLogger.error("Tocker or FIGI must be defined!")
3090            raise Exception("Ticker or FIGI required")
3091
3092        response = {}
3093        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3094        self.ticker = instrument["ticker"]
3095        self.figi = instrument["figi"]
3096
3097        if orderType == "Limit":
3098            uLogger.debug(
3099                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3100                    self.ticker, self.figi,
3101                    operation, lots, targetPrice, instrument["currency"],
3102                ))
3103
3104            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3105            self.body = str({
3106                "figi": self.figi,
3107                "quantity": str(lots),
3108                "price": FloatToNano(targetPrice),
3109                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3110                "accountId": str(self.accountId),
3111                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3112            })
3113            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3114
3115            if "orderId" in response.keys():
3116                uLogger.info(
3117                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3118                        response["orderId"],
3119                        self.ticker, self.figi,
3120                        operation, lots, targetPrice, instrument["currency"],
3121                    ))
3122
3123                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3124                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3125                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3126                            targetPrice, instrument["currency"],
3127                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3128                        ))
3129
3130                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3131                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3132                            targetPrice, instrument["currency"],
3133                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3134                        ))
3135
3136            else:
3137                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3138
3139        if orderType == "Stop":
3140            uLogger.debug(
3141                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3142                    self.ticker, self.figi,
3143                    operation, lots,
3144                    targetPrice, instrument["currency"],
3145                    limitPrice, instrument["currency"],
3146                    stopType, expDate,
3147                ))
3148
3149            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3150            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3151            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3152
3153            body = {
3154                "figi": self.figi,
3155                "quantity": str(lots),
3156                "price": FloatToNano(limitPrice),
3157                "stopPrice": FloatToNano(targetPrice),
3158                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3159                "accountId": str(self.accountId),
3160                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3161                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3162            }
3163
3164            if expDateUTC:
3165                body["expireDate"] = expDateUTC
3166
3167            self.body = str(body)
3168            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3169
3170            if "stopOrderId" in response.keys():
3171                uLogger.info(
3172                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3173                        response["stopOrderId"],
3174                        self.ticker, self.figi,
3175                        operation, lots,
3176                        targetPrice, instrument["currency"],
3177                        limitPrice, instrument["currency"],
3178                        TKS_STOP_ORDER_TYPES[stopOrderType],
3179                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3180                    ))
3181
3182                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3183                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3184                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3185                            targetPrice, instrument["currency"],
3186                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3187                        ))
3188
3189                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3190                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3191                            targetPrice, instrument["currency"],
3192                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3193                        ))
3194
3195            else:
3196                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3197
3198        return response
3199
3200    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3201        """
3202        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3203        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3204        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3205        See also: `Order()` docstring.
3206
3207        :param lots: volume, integer count of lots >= 1.
3208        :param targetPrice: target price > 0. This is open trade price for limit order.
3209        :return: JSON with response from broker server.
3210        """
3211        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3212
3213    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3214        """
3215        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3216        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3217        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3218        target price value then broker opens a limit order. See also: `Order()` docstring.
3219
3220        :param lots: volume, integer count of lots >= 1.
3221        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3222        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3223                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3224        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3225                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3226        :param expDate: string "Undefined" by default or local date in future.
3227                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3228                        This date is converting to UTC format for server.
3229        :return: JSON with response from broker server.
3230        """
3231        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3232
3233    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3234        """
3235        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3236        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3237        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3238        See also: `Order()` docstring.
3239
3240        :param lots: volume, integer count of lots >= 1.
3241        :param targetPrice: target price > 0. This is open trade price for limit order.
3242        :return: JSON with response from broker server.
3243        """
3244        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3245
3246    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3247        """
3248        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3249        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3250        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3251        target price value then broker opens a limit order. See also: `Order()` docstring.
3252
3253        :param lots: volume, integer count of lots >= 1.
3254        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3255        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3256                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3257        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3258                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3259        :param expDate: string "Undefined" by default or local date in future.
3260                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3261                        This date is converting to UTC format for server.
3262        :return: JSON with response from broker server.
3263        """
3264        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3265
3266    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3267        """
3268        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3269
3270        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3271        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3272                             This avoids unnecessary downloading data from the server.
3273        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3274        """
3275        if self.accountId is None or not self.accountId:
3276            uLogger.error("Variable `accountId` must be defined for using this method!")
3277            raise Exception("Account ID required")
3278
3279        if orderIDs:
3280            if allOrdersIDs is None:
3281                rawOrders = self.RequestPendingOrders()
3282                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3283
3284            if allStopOrdersIDs is None:
3285                rawStopOrders = self.RequestStopOrders()
3286                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3287
3288            for orderID in orderIDs:
3289                idInPendingOrders = orderID in allOrdersIDs
3290                idInStopOrders = orderID in allStopOrdersIDs
3291
3292                if not (idInPendingOrders or idInStopOrders):
3293                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3294                    continue
3295
3296                else:
3297                    if idInPendingOrders:
3298                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3299
3300                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3301                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3302                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3303                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3304
3305                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3306                            if self.moreDebug:
3307                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3308
3309                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3310
3311                        else:
3312                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3313
3314                    elif idInStopOrders:
3315                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3316
3317                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3318                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3319                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3320                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3321
3322                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3323                            if self.moreDebug:
3324                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3325
3326                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3327
3328                        else:
3329                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3330
3331                    else:
3332                        continue
3333
3334    def CloseAllOrders(self) -> None:
3335        """
3336        Gets a list of open pending and stop orders and cancel it all.
3337        """
3338        rawOrders = self.RequestPendingOrders()
3339        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3340        lenOrders = len(allOrdersIDs)
3341
3342        rawStopOrders = self.RequestStopOrders()
3343        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3344        lenSOrders = len(allStopOrdersIDs)
3345
3346        if lenOrders > 0 or lenSOrders > 0:
3347            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3348
3349            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3350
3351        else:
3352            uLogger.info("Orders not found, nothing to cancel.")
3353
3354    def CloseAll(self, *args) -> None:
3355        """
3356        Close all available (not blocked) opened trades and orders.
3357
3358        Also, you can select one or more keywords case-insensitive:
3359        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3360
3361        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3362        """
3363        overview = self.Overview(show=False)  # get all open trades info
3364
3365        if len(args) == 0:
3366            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3367            self.CloseAllOrders()  # close all pending and stop orders
3368
3369            for iType in TKS_INSTRUMENTS:
3370                if iType != "Currencies":
3371                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3372
3373        else:
3374            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3375            lowerArgs = [x.lower() for x in args]
3376
3377            if "orders" in lowerArgs:
3378                self.CloseAllOrders()  # close all pending and stop orders
3379
3380            for iType in TKS_INSTRUMENTS:
3381                if iType.lower() in lowerArgs and iType != "Currencies":
3382                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3383
3384    def CloseAllByTicker(self, instrument: str) -> None:
3385        """
3386        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3387
3388        This method searches opened trade and orders of instrument throw all portfolio and then use
3389        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3390
3391        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3392
3393        :param instrument: string with ticker.
3394        """
3395        if instrument is None or not instrument:
3396            uLogger.error("Ticker name must be defined for using this method!")
3397            raise Exception("Ticker required")
3398
3399        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3400
3401        self.ticker = instrument  # try to set instrument as ticker
3402        self.figi = ""
3403
3404        if self.IsInPortfolio(portfolio=overview):
3405            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3406            self.CloseTrades(instruments=[instrument], portfolio=overview)
3407
3408        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3409        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3410
3411        if limitAll and self.IsInLimitOrders(portfolio=overview):
3412            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3413            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3414
3415        if stopAll and self.IsInStopOrders(portfolio=overview):
3416            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3417            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3418
3419    def CloseAllByFIGI(self, instrument: str) -> None:
3420        """
3421        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3422
3423        This method searches opened trade and orders of instrument throw all portfolio and then use
3424        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3425
3426        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3427
3428        :param instrument: string with FIGI id.
3429        """
3430        if instrument is None or not instrument:
3431            uLogger.error("FIGI id must be defined for using this method!")
3432            raise Exception("FIGI required")
3433
3434        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3435
3436        self.ticker = ""
3437        self.figi = instrument  # try to set instrument as FIGI id
3438
3439        if self.IsInPortfolio(portfolio=overview):
3440            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3441            self.CloseTrades(instruments=[instrument], portfolio=overview)
3442
3443        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3444        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3445
3446        if limitAll and self.IsInLimitOrders(portfolio=overview):
3447            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3448            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3449
3450        if stopAll and self.IsInStopOrders(portfolio=overview):
3451            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3452            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3453
3454    @staticmethod
3455    def ParseOrderParameters(operation, **inputParameters):
3456        """
3457        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3458
3459        :param operation: string "Buy" or "Sell".
3460        :param inputParameters: this is dict of strings that looks like this
3461               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3462               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3463               "prices" key: one or more prices to open limit-orders
3464               Counts of values in lots and prices lists must be equals!
3465        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3466        """
3467        # TODO: update order grid work with api v2
3468        pass
3469        # uLogger.debug("Input parameters: {}".format(inputParameters))
3470        #
3471        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3472        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3473        #     raise Exception("Incorrect value")
3474        #
3475        # if "l" in inputParameters.keys():
3476        #     inputParameters["lots"] = inputParameters.pop("l")
3477        #
3478        # if "p" in inputParameters.keys():
3479        #     inputParameters["prices"] = inputParameters.pop("p")
3480        #
3481        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3482        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3483        #     raise Exception("Incorrect value")
3484        #
3485        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3486        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3487        #
3488        # if len(lots) != len(prices):
3489        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3490        #     raise Exception("Incorrect value")
3491        #
3492        # uLogger.debug("Extracted parameters for orders:")
3493        # uLogger.debug("lots = {}".format(lots))
3494        # uLogger.debug("prices = {}".format(prices))
3495        #
3496        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3497        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3498        # uLogger.debug("Order parameters: {}".format(result))
3499        #
3500        # return result
3501
3502    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3503        """
3504        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3505
3506        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3507        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3508        """
3509        result = False
3510        msg = "Instrument not defined!"
3511
3512        if portfolio is None or not portfolio:
3513            portfolio = self.Overview(show=False)
3514
3515        if self.ticker:
3516            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self.ticker))
3517            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3518
3519            for iType in TKS_INSTRUMENTS:
3520                for instrument in portfolio["stat"][iType]:
3521                    if instrument["ticker"] == self.ticker:
3522                        result = True
3523                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3524                        break
3525
3526        elif self.figi:
3527            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self.figi))
3528            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["figi"] == self.figi:
3533                        result = True
3534                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3535                        break
3536
3537        else:
3538            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3539
3540        uLogger.debug(msg)
3541
3542        return result
3543
3544    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3545        """
3546        Returns instrument from the user's portfolio if it presents there.
3547        Instrument must be defined by `ticker` (highly priority) or `figi`.
3548
3549        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3550        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3551        """
3552        result = None
3553        msg = "Instrument not defined!"
3554
3555        if portfolio is None or not portfolio:
3556            portfolio = self.Overview(show=False)
3557
3558        if self.ticker:
3559            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3560            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3561
3562            for iType in TKS_INSTRUMENTS:
3563                for instrument in portfolio["stat"][iType]:
3564                    if instrument["ticker"] == self.ticker:
3565                        result = instrument
3566                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3567                        break
3568
3569        elif self.figi:
3570            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3571            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3572
3573            for iType in TKS_INSTRUMENTS:
3574                for instrument in portfolio["stat"][iType]:
3575                    if instrument["figi"] == self.figi:
3576                        result = instrument
3577                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3578                        break
3579
3580        else:
3581            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3582
3583        uLogger.debug(msg)
3584
3585        return result
3586
3587    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3588        """
3589        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3590
3591        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3592
3593        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3594        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3595        """
3596        result = False
3597        msg = "Instrument not defined!"
3598
3599        if portfolio is None or not portfolio:
3600            portfolio = self.Overview(show=False)
3601
3602        if self.ticker:
3603            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker))
3604            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker)
3605
3606            for instrument in portfolio["stat"]["orders"]:
3607                if instrument["ticker"] == self.ticker:
3608                    result = True
3609                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker)
3610                    break
3611
3612        elif self.figi:
3613            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi))
3614            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi)
3615
3616            for instrument in portfolio["stat"]["orders"]:
3617                if instrument["figi"] == self.figi:
3618                    result = True
3619                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi)
3620                    break
3621
3622        else:
3623            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3624
3625        uLogger.debug(msg)
3626
3627        return result
3628
3629    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3630        """
3631        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3632        Instrument must be defined by `ticker` (highly priority) or `figi`.
3633
3634        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3635
3636        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3637        :return: list with `orderID`s of limit orders.
3638        """
3639        result = []
3640        msg = "Instrument not defined!"
3641
3642        if portfolio is None or not portfolio:
3643            portfolio = self.Overview(show=False)
3644
3645        if self.ticker:
3646            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker))
3647            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker)
3648
3649            for instrument in portfolio["stat"]["orders"]:
3650                if instrument["ticker"] == self.ticker:
3651                    result.append(instrument["orderID"])
3652
3653            if result:
3654                msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker)
3655
3656        elif self.figi:
3657            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi))
3658            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi)
3659
3660            for instrument in portfolio["stat"]["orders"]:
3661                if instrument["figi"] == self.figi:
3662                    result.append(instrument["orderID"])
3663
3664            if result:
3665                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi)
3666
3667        else:
3668            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3669
3670        uLogger.debug(msg)
3671
3672        return result
3673
3674    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3675        """
3676        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3677
3678        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3679
3680        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3681        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3682        """
3683        result = False
3684        msg = "Instrument not defined!"
3685
3686        if portfolio is None or not portfolio:
3687            portfolio = self.Overview(show=False)
3688
3689        if self.ticker:
3690            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker))
3691            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker)
3692
3693            for instrument in portfolio["stat"]["stopOrders"]:
3694                if instrument["ticker"] == self.ticker:
3695                    result = True
3696                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker)
3697                    break
3698
3699        elif self.figi:
3700            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi))
3701            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi)
3702
3703            for instrument in portfolio["stat"]["stopOrders"]:
3704                if instrument["figi"] == self.figi:
3705                    result = True
3706                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi)
3707                    break
3708
3709        else:
3710            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3711
3712        uLogger.debug(msg)
3713
3714        return result
3715
3716    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3717        """
3718        Returns list with all `orderID`s of opened stop orders for the instrument.
3719        Instrument must be defined by `ticker` (highly priority) or `figi`.
3720
3721        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3722
3723        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3724        :return: list with `orderID`s of stop orders.
3725        """
3726        result = []
3727        msg = "Instrument not defined!"
3728
3729        if portfolio is None or not portfolio:
3730            portfolio = self.Overview(show=False)
3731
3732        if self.ticker:
3733            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker))
3734            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker)
3735
3736            for instrument in portfolio["stat"]["stopOrders"]:
3737                if instrument["ticker"] == self.ticker:
3738                    result.append(instrument["orderID"])
3739
3740            if result:
3741                msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker)
3742
3743        elif self.figi:
3744            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi))
3745            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi)
3746
3747            for instrument in portfolio["stat"]["stopOrders"]:
3748                if instrument["figi"] == self.figi:
3749                    result.append(instrument["orderID"])
3750
3751            if result:
3752                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi)
3753
3754        else:
3755            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3756
3757        uLogger.debug(msg)
3758
3759        return result
3760
3761    def RequestLimits(self) -> dict:
3762        """
3763        Method for obtaining the available funds for withdrawal for current `accountId`.
3764
3765        See also:
3766        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3767        - `OverviewLimits()` method
3768
3769        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3770                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3771                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3772                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3773        """
3774        if self.accountId is None or not self.accountId:
3775            uLogger.error("Variable `accountId` must be defined for using this method!")
3776            raise Exception("Account ID required")
3777
3778        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3779
3780        self.body = str({"accountId": self.accountId})
3781        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3782        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3783
3784        if self.moreDebug:
3785            uLogger.debug("Records about available funds for withdrawal successfully received")
3786
3787        return rawLimits
3788
3789    def OverviewLimits(self, show: bool = False) -> dict:
3790        """
3791        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3792
3793        See also: `RequestLimits()`.
3794
3795        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3796        :return: dict with raw parsed data from server and some calculated statistics about it.
3797        """
3798        if self.accountId is None or not self.accountId:
3799            uLogger.error("Variable `accountId` must be defined for using this method!")
3800            raise Exception("Account ID required")
3801
3802        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3803
3804        view = {
3805            "rawLimits": rawLimits,
3806            "limits": {  # parsed data for every currency:
3807                "money": {  # this is an array of portfolio currency positions
3808                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3809                },
3810                "blocked": {  # this is an array of blocked currency
3811                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3812                },
3813                "blockedGuarantee": {  # this is locked money under collateral for futures
3814                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3815                },
3816            },
3817        }
3818
3819        # --- Prepare text table with limits in human-readable format:
3820        if show:
3821            info = [
3822                "# Withdrawal limits\n\n",
3823                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3824                "* **Account ID:** [{}]\n".format(self.accountId),
3825            ]
3826
3827            if view["limits"]["money"]:
3828                info.extend([
3829                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3830                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3831                ])
3832
3833            else:
3834                info.append("\nNo withdrawal limits\n")
3835
3836            for curr in view["limits"]["money"].keys():
3837                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3838                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3839                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3840
3841                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3842                    "[{}]".format(curr),
3843                    "{:.2f}".format(view["limits"]["money"][curr]),
3844                    "{:.2f}".format(availableMoney),
3845                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3846                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3847                )
3848
3849                if curr == "rub":
3850                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3851
3852                else:
3853                    info.append(infoStr)
3854
3855            infoText = "".join(info)
3856
3857            uLogger.info(infoText)
3858
3859            if self.withdrawalLimitsFile:
3860                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3861                    fH.write(infoText)
3862
3863                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3864
3865        return view
3866
3867    def RequestAccounts(self) -> dict:
3868        """
3869        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3870
3871        See also:
3872        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3873        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3874        - `OverviewUserInfo()` method
3875
3876        :return: dict with raw data from server that contains accounts info. Example of dict:
3877                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3878                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3879                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3880                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3881        """
3882        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3883
3884        self.body = str({})
3885        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3886        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3887
3888        if self.moreDebug:
3889            uLogger.debug("Records about available accounts successfully received")
3890
3891        return rawAccounts
3892
3893    def RequestUserInfo(self) -> dict:
3894        """
3895        Method for requesting common user's information.
3896
3897        See also:
3898        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3899        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3900        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3901        - `OverviewUserInfo()` method
3902
3903        :return: dict with raw data from server that contains user's information. Example of dict:
3904                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3905                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3906        """
3907        uLogger.debug("Requesting common user's information. Wait, please...")
3908
3909        self.body = str({})
3910        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3911        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3912
3913        if self.moreDebug:
3914            uLogger.debug("Records about current user successfully received")
3915
3916        return rawUserInfo
3917
3918    def RequestMarginStatus(self, accountId: str = None) -> dict:
3919        """
3920        Method for requesting margin calculation for defined account ID.
3921
3922        See also:
3923        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3924        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3925        - `OverviewUserInfo()` method
3926
3927        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3928        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3929                 Example of responses:
3930                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3931                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3932                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3933                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3934                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3935                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3936        """
3937        if accountId is None or not accountId:
3938            if self.accountId is None or not self.accountId:
3939                uLogger.error("Variable `accountId` must be defined for using this method!")
3940                raise Exception("Account ID required")
3941
3942            else:
3943                accountId = self.accountId  # use `self.accountId` (main ID) by default
3944
3945        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3946
3947        self.body = str({"accountId": accountId})
3948        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3949        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3950
3951        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3952            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3953            rawMargin = {}
3954
3955        else:
3956            if self.moreDebug:
3957                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3958
3959        return rawMargin
3960
3961    def RequestTariffLimits(self) -> dict:
3962        """
3963        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3964
3965        See also:
3966        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3967        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3968        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3969        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3970        - `OverviewUserInfo()` method
3971
3972        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3973                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3974                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3975        """
3976        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3977
3978        self.body = str({})
3979        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3980        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3981
3982        if self.moreDebug:
3983            uLogger.debug("Records with limits of current tariff successfully received")
3984
3985        return rawTariffLimits
3986
3987    def RequestBondCoupons(self, iJSON: dict) -> dict:
3988        """
3989        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3990        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3991        All dates are in UTC timezone.
3992
3993        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3994        Documentation:
3995        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3996        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3997
3998        See also: `ExtendBondsData()`.
3999
4000        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
4001                      If raw iJSON is not data of bond then server returns an error [400] with message:
4002                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4003        :return: dictionary with bond payment calendar. Response example
4004                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4005                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4006                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4007                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4008        """
4009        if iJSON["figi"] is None or not iJSON["figi"]:
4010            uLogger.error("FIGI must be defined for using this method!")
4011            raise Exception("FIGI required")
4012
4013        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4014        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4015
4016        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4017            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4018            self.figi,
4019            startDate,
4020            endDate,
4021        ))
4022
4023        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4024        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4025        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4026
4027        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4028            uLogger.warning("Instrument type is not bond!")
4029
4030        else:
4031            if self.moreDebug:
4032                uLogger.debug("Records about bond payment calendar successfully received")
4033
4034        return calendar
4035
4036    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4037        """
4038        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4039        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4040        coupon yields, current yields and some statistics etc.
4041
4042        WARNING! This is too long operation if a lot of bonds requested from broker server.
4043
4044        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4045
4046        :param instruments: list of strings with tickers or FIGIs.
4047        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4048                     for further used by data scientists or stock analytics.
4049        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4050                 In XLSX-file and Pandas DataFrame fields mean:
4051                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4052                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4053        """
4054        if instruments is None or not instruments:
4055            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4056            raise Exception("Ticker or FIGI required")
4057
4058        if isinstance(instruments, str):
4059            instruments = [instruments]
4060
4061        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4062
4063        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4064
4065        iCount = len(uniqueInstruments)
4066        tooLong = iCount >= 20
4067        if tooLong:
4068            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4069
4070        bonds = None
4071        for i, self.figi in enumerate(uniqueInstruments):
4072            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4073
4074            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4075                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4076                rawBond = self.SearchByFIGI(requestPrice=True)
4077
4078                # Widen raw data with UTC current time (iData["actualDateTime"]):
4079                actualDate = datetime.now(tzutc())
4080                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4081
4082                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4083                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4084
4085                # Replace some values with human-readable:
4086                iData["nominalCurrency"] = iData["nominal"]["currency"]
4087                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4088                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4089                iData["aciCurrency"] = iData["aciValue"]["currency"]
4090                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4091                iData["issueSize"] = int(iData["issueSize"])
4092                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4093                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4094                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4095                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4096                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4097                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4098                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4099                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4100                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4101                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4102
4103                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4104                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4105                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4106                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4107                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4108                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4109                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4110                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4111                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4112                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4113                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4114
4115                # Widen raw data with calendar data from `rawCalendar` values:
4116                calendarData = []
4117                if "events" in iData["rawCalendar"].keys():
4118                    for item in iData["rawCalendar"]["events"]:
4119                        calendarData.append({
4120                            "couponDate": item["couponDate"],
4121                            "couponNumber": int(item["couponNumber"]),
4122                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4123                            "payCurrency": item["payOneBond"]["currency"],
4124                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4125                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4126                            "couponStartDate": item["couponStartDate"],
4127                            "couponEndDate": item["couponEndDate"],
4128                            "couponPeriod": item["couponPeriod"],
4129                        })
4130
4131                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4132                    if "maturityDate" not in iData.keys():
4133                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4134
4135                # Widen raw data with Coupon Rate.
4136                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4137                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4138                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4139                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4140
4141                # Widen raw data with Yield to Maturity (YTM) on current date.
4142                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4143                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4144                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4145                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4146                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4147                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4148
4149                iData["calendar"] = calendarData  # adds calendar at the end
4150
4151                # Remove not used data:
4152                iData.pop("uid")
4153                iData.pop("positionUid")
4154                iData.pop("currentPrice")
4155                iData.pop("rawCalendar")
4156
4157                colNames = list(iData.keys())
4158                if bonds is None:
4159                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4160
4161                else:
4162                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4163
4164            else:
4165                uLogger.warning("Instrument is not a bond!")
4166
4167            processed = round(100 * (i + 1) / iCount, 1)
4168            if tooLong and processed % 5 == 0:
4169                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4170
4171            else:
4172                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4173
4174        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4175
4176        # Saving bonds from Pandas DataFrame to XLSX sheet:
4177        if xlsx and self.bondsXLSXFile:
4178            with pd.ExcelWriter(
4179                    path=self.bondsXLSXFile,
4180                    date_format=TKS_DATE_FORMAT,
4181                    datetime_format=TKS_DATE_TIME_FORMAT,
4182                    mode="w",
4183            ) as writer:
4184                bonds.to_excel(
4185                    writer,
4186                    sheet_name="Extended bonds data",
4187                    index=True,
4188                    encoding="UTF-8",
4189                    freeze_panes=(1, 1),
4190                )  # saving as XLSX-file with freeze first row and column as headers
4191
4192            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4193
4194        return bonds
4195
4196    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4197        """
4198        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4199
4200        WARNING! This is too long operation if a lot of bonds requested from broker server.
4201
4202        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4203
4204        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4205                        extended information about bonds: main info, current prices, bond payment calendar,
4206                        coupon yields, current yields and some statistics etc.
4207                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4208        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4209                     for further used by data scientists or stock analytics.
4210        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4211        """
4212        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4213            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4214
4215        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4216
4217        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4218        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4219        calendar = None
4220        for bond in extBonds.iterrows():
4221            for item in bond[1]["calendar"]:
4222                cData = {
4223                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4224                    "couponDate": item["couponDate"],
4225                    "figi": bond[1]["figi"],
4226                    "ticker": bond[1]["ticker"],
4227                    "name": bond[1]["name"],
4228                    "couponNumber": item["couponNumber"],
4229                    "payOneBond": item["payOneBond"],
4230                    "payCurrency": item["payCurrency"],
4231                    "couponType": item["couponType"],
4232                    "couponPeriod": item["couponPeriod"],
4233                    "fixDate": item["fixDate"],
4234                    "couponStartDate": item["couponStartDate"],
4235                    "couponEndDate": item["couponEndDate"],
4236                }
4237
4238                if calendar is None:
4239                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4240
4241                else:
4242                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4243
4244        if calendar is not None:
4245            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4246
4247            # Saving calendar from Pandas DataFrame to XLSX sheet:
4248            if xlsx:
4249                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4250
4251                with pd.ExcelWriter(
4252                        path=xlsxCalendarFile,
4253                        date_format=TKS_DATE_FORMAT,
4254                        datetime_format=TKS_DATE_TIME_FORMAT,
4255                        mode="w",
4256                ) as writer:
4257                    humanReadable = calendar.copy(deep=True)
4258                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4259                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4260                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4261                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4262                    humanReadable.columns = colNames  # human-readable column names
4263
4264                    humanReadable.to_excel(
4265                        writer,
4266                        sheet_name="Bond payments calendar",
4267                        index=False,
4268                        encoding="UTF-8",
4269                        freeze_panes=(1, 2),
4270                    )  # saving as XLSX-file with freeze first row and column as headers
4271
4272                    del humanReadable  # release df in memory
4273
4274                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4275
4276        return calendar
4277
4278    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4279        """
4280        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4281        Also, creates Markdown file with calendar data, `calendar.md` by default.
4282
4283        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4284
4285        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4286                        extended information about bonds: main info, current prices, bond payment calendar,
4287                        coupon yields, current yields and some statistics etc.
4288                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4289        :param show: if `True` then also printing bonds payment calendar to the console,
4290                     otherwise save to file `calendarFile` only. `False` by default.
4291        :return: multilines text in Markdown format with bonds payment calendar as a table.
4292        """
4293        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4294            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4295
4296        infoText = "# Bond payments calendar\n\n"
4297
4298        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4299
4300        if not (calendar is None or calendar.empty):
4301            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4302
4303            info = [
4304                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4305                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4306            ]
4307
4308            newMonth = False
4309            notOneBond = calendar["figi"].nunique() > 1
4310            for i, bond in enumerate(calendar.iterrows()):
4311                if newMonth and notOneBond:
4312                    info.append(splitLine)
4313
4314                info.append(
4315                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4316                        "  √" if bond[1]["paid"] else "  —",
4317                        bond[1]["couponDate"].split("T")[0],
4318                        bond[1]["figi"],
4319                        bond[1]["ticker"],
4320                        bond[1]["couponNumber"],
4321                        "{} {}".format(
4322                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4323                            bond[1]["payCurrency"],
4324                        ),
4325                        bond[1]["couponType"],
4326                        bond[1]["couponPeriod"],
4327                        bond[1]["fixDate"].split("T")[0],
4328                    )
4329                )
4330
4331                if i < len(calendar.values) - 1:
4332                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4333                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4334                    newMonth = False if curDate.month == nextDate.month else True
4335
4336                else:
4337                    newMonth = False
4338
4339            infoText += "".join(info)
4340
4341            if show:
4342                uLogger.info("{}".format(infoText))
4343
4344            if self.calendarFile is not None:
4345                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4346                    fH.write(infoText)
4347
4348                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4349
4350        else:
4351            infoText += "No data\n"
4352
4353        return infoText
4354
4355    def OverviewAccounts(self, show: bool = False) -> dict:
4356        """
4357        Method for parsing and show simple table with all available user accounts.
4358
4359        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4360
4361        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4362        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4363                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4364                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4365                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4366                                                        "closed": "—", "access": "Full access" }, ...}}`
4367        """
4368        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4369
4370        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4371        accounts = {
4372            item["id"]: {
4373                "type": TKS_ACCOUNT_TYPES[item["type"]],
4374                "name": item["name"],
4375                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4376                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4377                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4378                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4379            } for item in rawAccounts["accounts"]
4380        }
4381
4382        # Raw and parsed data with some fields replaced in "stat" section:
4383        view = {
4384            "rawAccounts": rawAccounts,
4385            "stat": accounts,
4386        }
4387
4388        # --- Prepare simple text table with only accounts data in human-readable format:
4389        if show:
4390            info = [
4391                "# User accounts\n\n",
4392                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4393                "| Account ID   | Type                      | Status                    | Name                           |\n",
4394                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4395            ]
4396
4397            for account in view["stat"].keys():
4398                info.extend([
4399                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4400                        account,
4401                        view["stat"][account]["type"],
4402                        view["stat"][account]["status"],
4403                        view["stat"][account]["name"],
4404                    )
4405                ])
4406
4407            infoText = "".join(info)
4408
4409            uLogger.info(infoText)
4410
4411            if self.userAccountsFile:
4412                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4413                    fH.write(infoText)
4414
4415                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4416
4417        return view
4418
4419    def OverviewUserInfo(self, show: bool = False) -> dict:
4420        """
4421        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4422
4423        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4424
4425        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4426        :return: dict with raw parsed data from server and some calculated statistics about it.
4427        """
4428        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4429        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4430        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4431        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4432        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4433        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4434
4435        # This is dict with parsed common user data:
4436        userInfo = {
4437            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4438            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4439            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4440            "tariff": rawUserInfo["tariff"],
4441        }
4442
4443        # This is an array of dict with parsed margin statuses for every account IDs:
4444        margins = {}
4445        for accountId in accounts.keys():
4446            if rawMargins[accountId]:
4447                margins[accountId] = {
4448                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4449                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4450                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4451                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4452                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4453                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4454                }
4455
4456            else:
4457                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4458
4459        unary = {}  # unary-connection limits
4460        for item in rawTariffLimits["unaryLimits"]:
4461            if item["limitPerMinute"] in unary.keys():
4462                unary[item["limitPerMinute"]].extend(item["methods"])
4463
4464            else:
4465                unary[item["limitPerMinute"]] = item["methods"]
4466
4467        stream = {}  # stream-connection limits
4468        for item in rawTariffLimits["streamLimits"]:
4469            if item["limit"] in stream.keys():
4470                stream[item["limit"]].extend(item["streams"])
4471
4472            else:
4473                stream[item["limit"]] = item["streams"]
4474
4475        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4476        limits = {
4477            "unary": unary,
4478            "stream": stream,
4479        }
4480
4481        # Raw and parsed data as an output result:
4482        view = {
4483            "rawUserInfo": rawUserInfo,
4484            "rawAccounts": rawAccounts,
4485            "rawMargins": rawMargins,
4486            "rawTariffLimits": rawTariffLimits,
4487            "stat": {
4488                "userInfo": userInfo,
4489                "accounts": accounts,
4490                "margins": margins,
4491                "limits": limits,
4492            },
4493        }
4494
4495        # --- Prepare text table with user information in human-readable format:
4496        if show:
4497            info = [
4498                "# Full user information\n\n",
4499                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4500                "## Common information\n\n",
4501                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4502                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4503                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4504                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4505                "\n## User accounts\n\n",
4506            ]
4507
4508            for account in view["stat"]["accounts"].keys():
4509                info.extend([
4510                    "### ID: [{}]\n\n".format(account),
4511                    "| Parameters           | Values                                                       |\n",
4512                    "|----------------------|--------------------------------------------------------------|\n",
4513                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4514                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4515                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4516                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4517                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4518                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4519                ])
4520
4521                if margins[account]:
4522                    info.extend([
4523                        "| Margin status:       | Enabled                                                      |\n",
4524                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4525                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4526                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4527                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4528                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4529                    ])
4530
4531                else:
4532                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4533
4534            info.extend([
4535                "\n## Current user tariff limits\n",
4536                "\nSee also:\n",
4537                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4538                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4539                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4540                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4541                "\n### Unary limits\n",
4542            ])
4543
4544            if unary:
4545                for key, values in sorted(unary.items()):
4546                    info.append("\n* Max requests per minute: {}\n".format(key))
4547
4548                    for value in values:
4549                        info.append("  - {}\n".format(value))
4550
4551            else:
4552                info.append("\nNot available\n")
4553
4554            info.append("\n### Stream limits\n")
4555
4556            if stream:
4557                for key, values in sorted(stream.items()):
4558                    info.append("\n* Max stream connections: {}\n".format(key))
4559
4560                    for value in values:
4561                        info.append("  - {}\n".format(value))
4562
4563            else:
4564                info.append("\nNot available\n")
4565
4566            infoText = "".join(info)
4567
4568            uLogger.info(infoText)
4569
4570            if self.userInfoFile:
4571                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4572                    fH.write(infoText)
4573
4574                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4575
4576        return view
4577
4578
4579class Args:
4580    """
4581    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4582    """
4583    def __init__(self, **kwargs):
4584        self.__dict__.update(kwargs)
4585
4586    def __getattr__(self, item):
4587        return None
4588
4589
4590def ParseArgs():
4591    """This function get and parse command line keys."""
4592    parser = ArgumentParser()  # command-line string parser
4593
4594    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4595    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4596
4597    # --- options:
4598
4599    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4600    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4601    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4602
4603    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4604    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4605
4606    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4607    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4608
4609    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4610
4611    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4612    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4613    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4614
4615    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4616    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4617
4618    # --- commands:
4619
4620    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4621
4622    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4623    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4624    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4625    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4626    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4627    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4628    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4629    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4630
4631    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4632    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4633    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4634    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4635    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4636    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4637
4638    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4639    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4640    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4641    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4642
4643    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4644    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4645    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4646
4647    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4648    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4649    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4650    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4651    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4652    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4653    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4654
4655    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4656    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4657    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4658    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4659    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4660
4661    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4662    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4663    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4664
4665    cmdArgs = parser.parse_args()
4666    return cmdArgs
4667
4668
4669def Main(**kwargs):
4670    """
4671    Main function for work with TKSBrokerAPI in the console.
4672
4673    See examples:
4674    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4675    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4676    """
4677    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4678
4679    if args.debug_level:
4680        uLogger.level = 10  # always debug level by default
4681        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4682
4683    exitCode = 0
4684    start = datetime.now(tzutc())
4685    uLogger.debug("=-" * 50)
4686    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4687        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4688        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4689    ))
4690
4691    # trying to calculate full current version:
4692    buildVersion = __version__
4693    try:
4694        v = version("tksbrokerapi")
4695        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4696
4697    except Exception:
4698        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4699
4700    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4701    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4702
4703    try:
4704        if args.version:
4705            print("TKSBrokerAPI {}".format(buildVersion))
4706            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4707
4708        else:
4709            # Init class for trading with Tinkoff Broker:
4710            trader = TinkoffBrokerServer(
4711                token=args.token,
4712                accountId=args.account_id,
4713                useCache=not args.no_cache,
4714            )
4715
4716            # --- set some options:
4717
4718            if args.more:
4719                trader.moreDebug = True
4720                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4721
4722            if args.ticker:
4723                ticker = args.ticker.upper()  # Tickers may be upper case only
4724
4725                if ticker in trader.aliasesKeys:
4726                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4727
4728                else:
4729                    trader.ticker = ticker
4730
4731            if args.figi:
4732                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4733
4734            if args.depth is not None:
4735                trader.depth = args.depth
4736
4737            # --- do one command:
4738
4739            if args.list:
4740                if args.output is not None:
4741                    trader.instrumentsFile = args.output
4742
4743                trader.ShowInstrumentsInfo(show=True)
4744
4745            elif args.list_xlsx:
4746                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4747
4748            elif args.bonds_xlsx is not None:
4749                if args.output is not None:
4750                    trader.bondsXLSXFile = args.output
4751
4752                if len(args.bonds_xlsx) == 0:
4753                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4754
4755                else:
4756                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4757
4758            elif args.search:
4759                if args.output is not None:
4760                    trader.searchResultsFile = args.output
4761
4762                trader.SearchInstruments(pattern=args.search[0], show=True)
4763
4764            elif args.info:
4765                if not (args.ticker or args.figi):
4766                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4767                    raise Exception("Ticker or FIGI required")
4768
4769                if args.output is not None:
4770                    trader.infoFile = args.output
4771
4772                if args.ticker:
4773                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4774
4775                else:
4776                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4777
4778            elif args.calendar is not None:
4779                if args.output is not None:
4780                    trader.calendarFile = args.output
4781
4782                if len(args.calendar) == 0:
4783                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4784
4785                else:
4786                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4787
4788                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4789
4790            elif args.price:
4791                if not (args.ticker or args.figi):
4792                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4793                    raise Exception("Ticker or FIGI required")
4794
4795                trader.GetCurrentPrices(show=True)
4796
4797            elif args.prices is not None:
4798                if args.output is not None:
4799                    trader.pricesFile = args.output
4800
4801                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4802
4803            elif args.overview:
4804                if args.output is not None:
4805                    trader.overviewFile = args.output
4806
4807                trader.Overview(show=True, details="full")
4808
4809            elif args.overview_digest:
4810                if args.output is not None:
4811                    trader.overviewDigestFile = args.output
4812
4813                trader.Overview(show=True, details="digest")
4814
4815            elif args.overview_positions:
4816                if args.output is not None:
4817                    trader.overviewPositionsFile = args.output
4818
4819                trader.Overview(show=True, details="positions")
4820
4821            elif args.overview_orders:
4822                if args.output is not None:
4823                    trader.overviewOrdersFile = args.output
4824
4825                trader.Overview(show=True, details="orders")
4826
4827            elif args.overview_analytics:
4828                if args.output is not None:
4829                    trader.overviewAnalyticsFile = args.output
4830
4831                trader.Overview(show=True, details="analytics")
4832
4833            elif args.overview_calendar:
4834                if args.output is not None:
4835                    trader.overviewAnalyticsFile = args.output
4836
4837                trader.Overview(show=True, details="calendar")
4838
4839            elif args.deals is not None:
4840                if args.output is not None:
4841                    trader.reportFile = args.output
4842
4843                if 0 <= len(args.deals) < 3:
4844                    trader.Deals(
4845                        start=args.deals[0] if len(args.deals) >= 1 else None,
4846                        end=args.deals[1] if len(args.deals) == 2 else None,
4847                        show=True,  # Always show deals report in console
4848                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4849                    )
4850
4851                else:
4852                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4853                    raise Exception("Incorrect value")
4854
4855            elif args.history is not None:
4856                if args.output is not None:
4857                    trader.historyFile = args.output
4858
4859                if 0 <= len(args.history) < 3:
4860                    dataReceived = trader.History(
4861                        start=args.history[0] if len(args.history) >= 1 else None,
4862                        end=args.history[1] if len(args.history) == 2 else None,
4863                        interval="hour" if args.interval is None or not args.interval else args.interval,
4864                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4865                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4866                        show=True,  # shows all downloaded candles in console
4867                    )
4868
4869                    if args.render_chart is not None and dataReceived is not None:
4870                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4871
4872                        trader.ShowHistoryChart(
4873                            candles=dataReceived,
4874                            interact=iChart,
4875                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4876                        )
4877
4878                else:
4879                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4880                    raise Exception("Incorrect value")
4881
4882            elif args.load_history is not None:
4883                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4884
4885                if args.render_chart is not None and histData is not None:
4886                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4887                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4888
4889                    trader.ShowHistoryChart(
4890                        candles=histData,
4891                        interact=iChart,
4892                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4893                    )
4894
4895            elif args.trade is not None:
4896                if 1 <= len(args.trade) <= 5:
4897                    trader.Trade(
4898                        operation=args.trade[0],
4899                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4900                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4901                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4902                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4903                    )
4904
4905                else:
4906                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4907
4908            elif args.buy is not None:
4909                if 0 <= len(args.buy) <= 4:
4910                    trader.Buy(
4911                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4912                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4913                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4914                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4915                    )
4916
4917                else:
4918                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4919
4920            elif args.sell is not None:
4921                if 0 <= len(args.sell) <= 4:
4922                    trader.Sell(
4923                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4924                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4925                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4926                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4927                    )
4928
4929                else:
4930                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4931
4932            elif args.order:
4933                if 4 <= len(args.order) <= 7:
4934                    trader.Order(
4935                        operation=args.order[0],
4936                        orderType=args.order[1],
4937                        lots=int(args.order[2]),
4938                        targetPrice=float(args.order[3]),
4939                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4940                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4941                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4942                    )
4943
4944                else:
4945                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4946
4947            elif args.buy_limit:
4948                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4949
4950            elif args.sell_limit:
4951                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4952
4953            elif args.buy_stop:
4954                if 2 <= len(args.buy_stop) <= 7:
4955                    trader.BuyStop(
4956                        lots=int(args.buy_stop[0]),
4957                        targetPrice=float(args.buy_stop[1]),
4958                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4959                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4960                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4961                    )
4962
4963                else:
4964                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4965
4966            elif args.sell_stop:
4967                if 2 <= len(args.sell_stop) <= 7:
4968                    trader.SellStop(
4969                        lots=int(args.sell_stop[0]),
4970                        targetPrice=float(args.sell_stop[1]),
4971                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4972                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4973                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4974                    )
4975
4976                else:
4977                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4978
4979            # elif args.buy_order_grid is not None:
4980            #     # update order grid work with api v2
4981            #     if len(args.buy_order_grid) == 2:
4982            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4983            #
4984            #         for order in orderParams:
4985            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4986            #
4987            #     else:
4988            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4989            #
4990            # elif args.sell_order_grid is not None:
4991            #     # update order grid work with api v2
4992            #     if len(args.sell_order_grid) >= 2:
4993            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4994            #
4995            #         for order in orderParams:
4996            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4997            #
4998            #     else:
4999            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5000
5001            elif args.close_order is not None:
5002                trader.CloseOrders(args.close_order)  # close only one order
5003
5004            elif args.close_orders is not None:
5005                trader.CloseOrders(args.close_orders)  # close list of orders
5006
5007            elif args.close_trade:
5008                if not (args.ticker or args.figi):
5009                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5010                    raise Exception("Ticker or FIGI required")
5011
5012                if args.ticker:
5013                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
5014
5015                else:
5016                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
5017
5018            elif args.close_trades is not None:
5019                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5020
5021            elif args.close_all is not None:
5022                if args.ticker:
5023                    trader.CloseAllByTicker(instrument=args.ticker)
5024
5025                elif args.figi:
5026                    trader.CloseAllByFIGI(instrument=args.figi)
5027
5028                else:
5029                    trader.CloseAll(*args.close_all)
5030
5031            elif args.limits:
5032                if args.output is not None:
5033                    trader.withdrawalLimitsFile = args.output
5034
5035                trader.OverviewLimits(show=True)
5036
5037            elif args.user_info:
5038                if args.output is not None:
5039                    trader.userInfoFile = args.output
5040
5041                trader.OverviewUserInfo(show=True)
5042
5043            elif args.account:
5044                if args.output is not None:
5045                    trader.userAccountsFile = args.output
5046
5047                trader.OverviewAccounts(show=True)
5048
5049            else:
5050                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5051                raise Exception("There is no command to execute")
5052
5053    except Exception:
5054        trace = tb.format_exc()
5055        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5056            if e in trace:
5057                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5058                break
5059
5060        uLogger.debug(trace)
5061        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5062        exitCode = 255  # an error occurred, must be open a ticket for this issue
5063
5064    finally:
5065        finish = datetime.now(tzutc())
5066
5067        if exitCode == 0:
5068            if args.more:
5069                uLogger.debug("All operations were finished success (summary code is 0).")
5070
5071        else:
5072            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5073                os.path.abspath(uLog.defaultLogFile), exitCode,
5074            ))
5075
5076        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5077        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5078            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5079            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5080        ))
5081        uLogger.debug("=-" * 50)
5082
5083        if not kwargs:
5084            sys.exit(exitCode)
5085
5086        else:
5087            return exitCode
5088
5089
5090if __name__ == "__main__":
5091    Main()
class TinkoffBrokerServer:
  76class TinkoffBrokerServer:
  77    """
  78    This class implements methods to work with Tinkoff broker server.
  79
  80    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  81
  82    About `token`: https://tinkoff.github.io/investAPI/token/
  83    """
  84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  85        """
  86        Main class init.
  87
  88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  91        :param useCache: use default cache file with raw data to use instead of `iList`.
  92                         True by default. Cache is auto-update if new day has come.
  93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  94        :param defaultCache: path to default cache file. `dump.json` by default.
  95        """
  96        if token is None or not token:
  97            try:
  98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 100
 101            except KeyError:
 102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 103                raise Exception("Token required")
 104
 105        else:
 106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 108
 109        if accountId is None or not accountId:
 110            try:
 111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 113
 114            except KeyError:
 115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 116
 117        else:
 118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 120
 121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 123
 124        Latest version: https://pypi.org/project/tksbrokerapi/
 125        """
 126
 127        self.aliases = TKS_TICKER_ALIASES
 128        """Some aliases instead official tickers.
 129
 130        See also: `TKSEnums.TKS_TICKER_ALIASES`
 131        """
 132
 133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 134
 135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 136
 137        self.ticker = ""
 138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 139
 140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 142
 143        See also: `SearchByTicker()`, `SearchInstruments()`.
 144        """
 145
 146        self.figi = ""
 147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 148
 149        See also: `SearchByFIGI()`, `SearchInstruments()`.
 150        """
 151
 152        self.depth = 1
 153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 154
 155        See also: `GetCurrentPrices()`.
 156        """
 157
 158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 160
 161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 162        """
 163
 164        uLogger.debug("Broker API server: {}".format(self.server))
 165
 166        self.timeout = 15
 167        """Server operations timeout in seconds. Default: `15`.
 168
 169        See also: `SendAPIRequest()`.
 170        """
 171
 172        self.headers = {
 173            "Content-Type": "application/json",
 174            "accept": "application/json",
 175            "Authorization": "Bearer {}".format(self.token),
 176            "x-app-name": "Tim55667757.TKSBrokerAPI",
 177        }
 178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 179
 180        See also: `SendAPIRequest()`.
 181        """
 182
 183        self.body = None
 184        """Request body which send to broker server. Default: `None`.
 185
 186        See also: `SendAPIRequest()`.
 187        """
 188
 189        self.moreDebug = False
 190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 191
 192        self.historyFile = None
 193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 194
 195        See also: `History()`.
 196        """
 197
 198        self.htmlHistoryFile = "index.html"
 199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 200
 201        See also: `ShowHistoryChart()`.
 202        """
 203
 204        self.instrumentsFile = "instruments.md"
 205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 206
 207        See also: `ShowInstrumentsInfo()`.
 208        """
 209
 210        self.searchResultsFile = "search-results.md"
 211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 212
 213        See also: `SearchInstruments()`.
 214        """
 215
 216        self.pricesFile = "prices.md"
 217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 218
 219        See also: `GetListOfPrices()`.
 220        """
 221
 222        self.infoFile = "info.md"
 223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 224
 225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 226        """
 227
 228        self.bondsXLSXFile = "ext-bonds.xlsx"
 229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 231
 232        See also: `ExtendBondsData()`.
 233        """
 234
 235        self.calendarFile = "calendar.md"
 236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 237        
 238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 239
 240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 241        """
 242
 243        self.overviewFile = "overview.md"
 244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 245
 246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 247        """
 248
 249        self.overviewDigestFile = "overview-digest.md"
 250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 251
 252        See also: `Overview()` with parameter `details="digest"`.
 253        """
 254
 255        self.overviewPositionsFile = "overview-positions.md"
 256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 257
 258        See also: `Overview()` with parameter `details="positions"`.
 259        """
 260
 261        self.overviewOrdersFile = "overview-orders.md"
 262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 263
 264        See also: `Overview()` with parameter `details="orders"`.
 265        """
 266
 267        self.overviewAnalyticsFile = "overview-analytics.md"
 268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 269
 270        See also: `Overview()` with parameter `details="analytics"`.
 271        """
 272
 273        self.overviewBondsCalendarFile = "overview-calendar.md"
 274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 275
 276        See also: `Overview()` with parameter `details="calendar"`.
 277        """
 278
 279        self.reportFile = "deals.md"
 280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 281
 282        See also: `Deals()`.
 283        """
 284
 285        self.withdrawalLimitsFile = "limits.md"
 286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 287
 288        See also: `OverviewLimits()` and `RequestLimits()`.
 289        """
 290
 291        self.userInfoFile = "user-info.md"
 292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 293
 294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 295        """
 296
 297        self.userAccountsFile = "accounts.md"
 298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 299
 300        See also: `OverviewAccounts()`, `RequestAccounts()`.
 301        """
 302
 303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 305
 306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 307
 308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 309        """
 310
 311        self.iList = None  # init iList for raw instruments data
 312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 313        
 314        See also: `Listing()`, `DumpInstruments()`.
 315        """
 316
 317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 318        if useCache:
 319            if os.path.exists(self.iListDumpFile):
 320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 321                curTime = datetime.now(tzutc())
 322
 323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 325
 326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 327
 328                else:
 329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 330
 331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 332                        os.path.abspath(self.iListDumpFile),
 333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 334                    ))
 335
 336            else:
 337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 339
 340        else:
 341            self.iList = self.Listing()  # request new raw instruments data from broker server
 342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 343
 344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 346
 347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 348        """
 349
 350    def _ParseJSON(self, rawData="{}") -> dict:
 351        """
 352        Parse JSON from response string.
 353
 354        :param rawData: this is a string with JSON-formatted text.
 355        :return: JSON (dictionary), parsed from server response string.
 356        """
 357        responseJSON = json.loads(rawData) if rawData else {}
 358
 359        if self.moreDebug:
 360            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 361
 362        return responseJSON
 363
 364    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 365        """
 366        Send GET or POST request to broker server and receive JSON object.
 367
 368        self.header: must be defining with dictionary of headers.
 369        self.body: if define then used as request body. None by default.
 370        self.timeout: global request timeout, 15 seconds by default.
 371        :param url: url with REST request.
 372        :param reqType: send "GET" or "POST" request. "GET" by default.
 373        :param retry: how many times retry after first request if an 5xx server errors occurred.
 374        :param pause: sleep time in seconds between retries.
 375        :return: response JSON (dictionary) from broker.
 376        """
 377        if reqType.upper() not in ("GET", "POST"):
 378            uLogger.error("You can define request type: `GET` or `POST`!")
 379            raise Exception("Incorrect value")
 380
 381        if self.moreDebug:
 382            uLogger.debug("Request parameters:")
 383            uLogger.debug("    - REST API URL: {}".format(url))
 384            uLogger.debug("    - request type: {}".format(reqType))
 385            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 386            uLogger.debug("    - body:\n{}".format(self.body))
 387
 388        # fast hack to avoid all operations with some tickers/FIGI
 389        responseJSON = {}
 390        oK = True
 391        for item in self.exclude:
 392            if item in url:
 393                if self.moreDebug:
 394                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 395
 396                oK = False
 397                break
 398
 399        if oK:
 400            counter = 0
 401            response = None
 402            errMsg = ""
 403
 404            while not response and counter <= retry:
 405                if reqType == "GET":
 406                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 407
 408                if reqType == "POST":
 409                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 410
 411                if self.moreDebug:
 412                    uLogger.debug("Response:")
 413                    uLogger.debug("    - status code: {}".format(response.status_code))
 414                    uLogger.debug("    - reason: {}".format(response.reason))
 415                    uLogger.debug("    - body length: {}".format(len(response.text)))
 416                    uLogger.debug("    - headers:\n{}".format(response.headers))
 417
 418                # Server returns some headers:
 419                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 420                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 421                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 422                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 423                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 424                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 425                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 426                    sleep(rateLimitWait)
 427
 428                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 429                if 400 <= response.status_code < 500:
 430                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 431                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 432
 433                    if "code" in response.text and "message" in response.text:
 434                        msgDict = self._ParseJSON(rawData=response.text)
 435                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 436
 437                    counter = retry + 1  # do not retry for 4xx errors
 438
 439                if 500 <= response.status_code < 600:
 440                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 441                    uLogger.debug("    - not oK, {}".format(errMsg))
 442
 443                    if "code" in response.text and "message" in response.text:
 444                        errMsgDict = self._ParseJSON(rawData=response.text)
 445                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 446
 447                    counter += 1
 448
 449                    if counter <= retry:
 450                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 451                        sleep(pause)
 452
 453            responseJSON = self._ParseJSON(rawData=response.text)
 454
 455            if errMsg:
 456                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 457                uLogger.error("    - not oK, {}".format(errMsg))
 458
 459        return responseJSON
 460
 461    def _IUpdater(self, iType: str) -> tuple:
 462        """
 463        Request instrument by type from server. See available API methods for instruments:
 464        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 465        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 466        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 467        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 468        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 469
 470        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 471        :return: tuple with iType name and list of available instruments of current type for defined user token.
 472        """
 473        result = []
 474
 475        if iType in TKS_INSTRUMENTS:
 476            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 477
 478            # all instruments have the same body in API v2 requests:
 479            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 480            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 481            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 482
 483        return iType, result
 484
 485    def _IWrapper(self, kwargs):
 486        """
 487        Wrapper runs instrument's update method `_IUpdater()`.
 488        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 489        """
 490        return self._IUpdater(**kwargs)
 491
 492    def Listing(self) -> dict:
 493        """
 494        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 495
 496        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 497        """
 498        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 499        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 500
 501        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 502        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 503        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 504
 505        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 506        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 507        poolUpdater.close()
 508
 509        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 510        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 511        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 512
 513        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 514        for iType in iList.keys():
 515            for ticker in iList[iType]:
 516                iList[iType][ticker]["type"] = iType
 517
 518                if "minPriceIncrement" in iList[iType][ticker].keys():
 519                    iList[iType][ticker]["step"] = NanoToFloat(
 520                        iList[iType][ticker]["minPriceIncrement"]["units"],
 521                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 522                    )
 523
 524                else:
 525                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 526
 527        return iList
 528
 529    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 530        """
 531        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 532
 533        See also: `DumpInstruments()`, `Listing()`.
 534
 535        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 536                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 537        """
 538        if self.iListDumpFile is None or not self.iListDumpFile:
 539            uLogger.error("Output name of dump file must be defined!")
 540            raise Exception("Filename required")
 541
 542        if not self.iList or forceUpdate:
 543            self.iList = self.Listing()
 544
 545        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 546
 547        # Save as XLSX with separated sheets for every type of instruments:
 548        with pd.ExcelWriter(
 549                path=xlsxDumpFile,
 550                date_format=TKS_DATE_FORMAT,
 551                datetime_format=TKS_DATE_TIME_FORMAT,
 552                mode="w",
 553        ) as writer:
 554            for iType in TKS_INSTRUMENTS:
 555                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 556                df = df[sorted(df)]  # sorted by column names
 557                df = df.applymap(
 558                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 559                    na_action="ignore",
 560                )  # converting numbers from nano-type to float in every cell
 561                df.to_excel(
 562                    writer,
 563                    sheet_name=iType,
 564                    encoding="UTF-8",
 565                    freeze_panes=(1, 1),
 566                )  # saving as XLSX-file with freeze first row and column as headers
 567
 568        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 569
 570    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 571        """
 572        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 573        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 574
 575        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 576
 577        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 578                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 579        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 580        """
 581        if self.iListDumpFile is None or not self.iListDumpFile:
 582            uLogger.error("Output name of dump file must be defined!")
 583            raise Exception("Filename required")
 584
 585        if not self.iList or forceUpdate:
 586            self.iList = self.Listing()
 587
 588        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 589        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 590            fH.write(jsonDump)
 591
 592        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 593
 594        return jsonDump
 595
 596    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 597        """
 598        Show information about one instrument defined by json data and prints it in Markdown format.
 599
 600        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 601
 602        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 603        :param show: if `True` then also printing information about instrument and its current price.
 604        :return: multilines text in Markdown format with information about one instrument.
 605        """
 606        splitLine = "|                                                             |                                                        |\n"
 607        infoText = ""
 608
 609        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 610            info = [
 611                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 612                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 613                "| Parameters                                                  | Values                                                 |\n",
 614                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 615                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 616                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 617            ]
 618
 619            if "sector" in iJSON.keys() and iJSON["sector"]:
 620                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 621
 622            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 623                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 624
 625            info.extend([
 626                splitLine,
 627                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 628                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 629            ])
 630
 631            if "isin" in iJSON.keys() and iJSON["isin"]:
 632                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 633
 634            if "classCode" in iJSON.keys():
 635                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 636
 637            info.extend([
 638                splitLine,
 639                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 640                splitLine,
 641                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 642                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 643                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 644            ])
 645
 646            if iJSON["figi"]:
 647                self.figi = iJSON["figi"]
 648                iJSON = iJSON | self.RequestTradingStatus()
 649
 650                info.extend([
 651                    splitLine,
 652                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 653                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 654                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 655                ])
 656
 657            info.append(splitLine)
 658
 659            if "type" in iJSON.keys() and iJSON["type"]:
 660                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 661
 662                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 663                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 664
 665            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 666                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 667
 668            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 669                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 670
 671            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 672                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 673
 674            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 675                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 676
 677            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 678                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 679
 680            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 681                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 682
 683            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 684                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 685
 686            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 687                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 688
 689            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 690                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 691
 692            if "currency" in iJSON.keys():
 693                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 694
 695            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 696                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 697
 698            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 699                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 700
 701            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 702                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 703
 704            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 705                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 706
 707            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 708                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 709
 710            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 711                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 712
 713            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 714                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 715
 716            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 717                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 718
 719            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 720                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 721
 722            iExt = None
 723            if iJSON["type"] == "Bonds":
 724                info.extend([
 725                    splitLine,
 726                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 727                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 728                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 729                        iJSON["nominal"]["currency"],
 730                    )),
 731                ])
 732
 733                if "floatingCouponFlag" in iJSON.keys():
 734                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 735
 736                if "amortizationFlag" in iJSON.keys():
 737                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 738
 739                info.append(splitLine)
 740
 741                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 742                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 743
 744                if iJSON["figi"]:
 745                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 746
 747                    info.extend([
 748                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 749                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 750                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 751                    ])
 752
 753                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 754                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 755                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 756                        iJSON["aciValue"]["currency"]
 757                    )))
 758
 759            if "currentPrice" in iJSON.keys():
 760                info.append(splitLine)
 761
 762                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 763                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 764
 765                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 766                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 767                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 768                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 769                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 770
 771                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 772                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 773
 774                info.extend([
 775                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 776                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 777                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 778                    )),
 779                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 780                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 781                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 782                    )),
 783                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 784                        "{:.2f}%{}".format(
 785                            iJSON["currentPrice"]["changes"],
 786                            " ({}{:.2f} {})".format(
 787                                "+" if bondChangesDelta > 0 else "",
 788                                bondChangesDelta,
 789                                aciCurrency
 790                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 791                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 792                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 793                                currency
 794                            ),
 795                        )
 796                    ),
 797                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 798                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 799                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 800                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 801                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 802                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 803                    )),
 804                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 805                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 806                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 807                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 808                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 809                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 810                    )),
 811                ])
 812
 813            if "lot" in iJSON.keys():
 814                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 815
 816            if "step" in iJSON.keys() and iJSON["step"] != 0:
 817                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 818
 819            # Add bond payment calendar:
 820            if iJSON["type"] == "Bonds":
 821                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 822                info.extend(["\n", strCalendar])
 823
 824            infoText += "".join(info)
 825
 826            if show:
 827                uLogger.info("{}".format(infoText))
 828
 829            else:
 830                uLogger.debug("{}".format(infoText))
 831
 832            if self.infoFile is not None:
 833                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 834                    fH.write(infoText)
 835
 836                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 837
 838        return infoText
 839
 840    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 841        """
 842        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 843
 844        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 845        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 846        :return: JSON formatted data with information about instrument.
 847        """
 848        tickerJSON = {}
 849        if self.moreDebug:
 850            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 851
 852        if not self.ticker:
 853            uLogger.warning("self.ticker variable is not be empty!")
 854
 855        else:
 856            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 857                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 858                raise Exception("Instrument not allowed")
 859
 860            if not self.iList:
 861                self.iList = self.Listing()
 862
 863            if self.ticker in self.iList["Shares"].keys():
 864                tickerJSON = self.iList["Shares"][self.ticker]
 865                if self.moreDebug:
 866                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 867
 868            elif self.ticker in self.iList["Currencies"].keys():
 869                tickerJSON = self.iList["Currencies"][self.ticker]
 870                if self.moreDebug:
 871                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 872
 873            elif self.ticker in self.iList["Bonds"].keys():
 874                tickerJSON = self.iList["Bonds"][self.ticker]
 875                if self.moreDebug:
 876                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 877
 878            elif self.ticker in self.iList["Etfs"].keys():
 879                tickerJSON = self.iList["Etfs"][self.ticker]
 880                if self.moreDebug:
 881                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 882
 883            elif self.ticker in self.iList["Futures"].keys():
 884                tickerJSON = self.iList["Futures"][self.ticker]
 885                if self.moreDebug:
 886                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 887
 888        if tickerJSON:
 889            self.figi = tickerJSON["figi"]
 890
 891            if requestPrice:
 892                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 893
 894                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 895                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 896
 897                else:
 898                    tickerJSON["currentPrice"]["changes"] = 0
 899
 900            if show:
 901                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 902
 903        else:
 904            if show:
 905                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 906
 907        return tickerJSON
 908
 909    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 910        """
 911        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 912
 913        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 914        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 915        :return: JSON formatted data with information about instrument.
 916        """
 917        figiJSON = {}
 918        if self.moreDebug:
 919            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 920
 921        if not self.figi:
 922            uLogger.warning("self.figi variable is not be empty!")
 923
 924        else:
 925            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 926                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 927                raise Exception("Instrument not allowed")
 928
 929            if not self.iList:
 930                self.iList = self.Listing()
 931
 932            for item in self.iList["Shares"].keys():
 933                if self.figi == self.iList["Shares"][item]["figi"]:
 934                    figiJSON = self.iList["Shares"][item]
 935
 936                    if self.moreDebug:
 937                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 938
 939                    break
 940
 941            if not figiJSON:
 942                for item in self.iList["Currencies"].keys():
 943                    if self.figi == self.iList["Currencies"][item]["figi"]:
 944                        figiJSON = self.iList["Currencies"][item]
 945
 946                        if self.moreDebug:
 947                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 948
 949                        break
 950
 951            if not figiJSON:
 952                for item in self.iList["Bonds"].keys():
 953                    if self.figi == self.iList["Bonds"][item]["figi"]:
 954                        figiJSON = self.iList["Bonds"][item]
 955
 956                        if self.moreDebug:
 957                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 958
 959                        break
 960
 961            if not figiJSON:
 962                for item in self.iList["Etfs"].keys():
 963                    if self.figi == self.iList["Etfs"][item]["figi"]:
 964                        figiJSON = self.iList["Etfs"][item]
 965
 966                        if self.moreDebug:
 967                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 968
 969                        break
 970
 971            if not figiJSON:
 972                for item in self.iList["Futures"].keys():
 973                    if self.figi == self.iList["Futures"][item]["figi"]:
 974                        figiJSON = self.iList["Futures"][item]
 975
 976                        if self.moreDebug:
 977                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 978
 979                        break
 980
 981        if figiJSON:
 982            self.figi = figiJSON["figi"]
 983            self.ticker = figiJSON["ticker"]
 984
 985            if requestPrice:
 986                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 987
 988                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 989                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 990
 991                else:
 992                    figiJSON["currentPrice"]["changes"] = 0
 993
 994            if show:
 995                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 996
 997        else:
 998            if show:
 999                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1000
1001        return figiJSON
1002
1003    def GetCurrentPrices(self, show: bool = True) -> dict:
1004        """
1005        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1006        `{"buy": [{"price": 1243.8, "quantity": 193},
1007                  {"price": 1244.0, "quantity": 168},
1008                  {"price": 1244.8, "quantity": 5},
1009                  {"price": 1245.0, "quantity": 61},
1010                  {"price": 1245.4, "quantity": 60}],
1011          "sell": [{"price": 1243.6, "quantity": 8},
1012                   {"price": 1242.6, "quantity": 10},
1013                   {"price": 1242.4, "quantity": 18},
1014                   {"price": 1242.2, "quantity": 50},
1015                   {"price": 1242.0, "quantity": 113}],
1016          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1017        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1018        - sell: list of dicts with Buyers prices,
1019            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1020            - quantity: volume value by current price in lots,
1021        - limitUp: current trade session limit price, maximum,
1022        - limitDown: current trade session limit price, minimum,
1023        - lastPrice: last deal price of the instrument,
1024        - closePrice: previous trade session close price of the instrument.
1025
1026        See also: `SearchByTicker()` and `SearchByFIGI()`.
1027        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1028        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1029
1030        :param show: if `True` then print DOM to log and console.
1031        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1032                 If an error occurred then returns an empty record:
1033                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1034        """
1035        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1036
1037        if self.depth < 1:
1038            uLogger.error("Depth of Market (DOM) must be >=1!")
1039            raise Exception("Incorrect value")
1040
1041        if not (self.ticker or self.figi):
1042            uLogger.error("self.ticker or self.figi variables must be defined!")
1043            raise Exception("Ticker or FIGI required")
1044
1045        if self.ticker and not self.figi:
1046            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1047            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1048
1049        if not self.ticker and self.figi:
1050            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1051            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1052
1053        if not self.figi:
1054            uLogger.error("FIGI is not defined!")
1055            raise Exception("Ticker or FIGI required")
1056
1057        else:
1058            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1059
1060            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1061            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1062            self.body = str({"figi": self.figi, "depth": self.depth})
1063            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1064
1065            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1066                # list of dicts with sellers orders:
1067                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1068
1069                # list of dicts with buyers orders:
1070                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1071
1072                # max price of instrument at this time:
1073                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1074
1075                # min price of instrument at this time:
1076                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1077
1078                # last price of deal with instrument:
1079                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1080
1081                # last close price of instrument:
1082                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1083
1084            else:
1085                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1086                uLogger.debug("Server response: {}".format(pricesResponse))
1087
1088            if show:
1089                if prices["buy"] or prices["sell"]:
1090                    info = [
1091                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1092                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1093                            self.ticker,
1094                            self.figi,
1095                            self.depth,
1096                        ),
1097                        "-" * 60, "\n",
1098                        "             Orders of Buyers | Orders of Sellers\n",
1099                        "-" * 60, "\n",
1100                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1101                        "-" * 60, "\n",
1102                    ]
1103
1104                    if not prices["buy"]:
1105                        info.append("                              | No orders!\n")
1106                        sumBuy = 0
1107
1108                    else:
1109                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1110                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1111                        for item in maxMinSorted:
1112                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1113
1114                    if not prices["sell"]:
1115                        info.append("No orders!                    |\n")
1116                        sumSell = 0
1117
1118                    else:
1119                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1120                        for item in prices["sell"]:
1121                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1122
1123                    info.extend([
1124                        "-" * 60, "\n",
1125                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1126                        "-" * 60, "\n",
1127                    ])
1128
1129                    infoText = "".join(info)
1130
1131                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1132
1133                else:
1134                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1135
1136        return prices
1137
1138    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1139        """
1140        This method get and show information about all available broker instruments for current user account.
1141        If `instrumentsFile` string is not empty then also save information to this file.
1142
1143        :param show: if `True` then print results to console, if `False` — print only to file.
1144        :return: multi-lines string with all available broker instruments
1145        """
1146        if not self.iList:
1147            self.iList = self.Listing()
1148
1149        info = [
1150            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1151            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1152        ]
1153
1154        # add instruments count by type:
1155        for iType in self.iList.keys():
1156            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1157
1158        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1159        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1160
1161        # generating info tables with all instruments by type:
1162        for iType in self.iList.keys():
1163            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1164
1165            for instrument in self.iList[iType].keys():
1166                iName = self.iList[iType][instrument]["name"]  # instrument's name
1167                if len(iName) > 57:
1168                    iName = "{}...".format(iName[:54])  # right trim for a long string
1169
1170                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1171                    self.iList[iType][instrument]["ticker"],
1172                    iName,
1173                    self.iList[iType][instrument]["figi"],
1174                    self.iList[iType][instrument]["currency"],
1175                    self.iList[iType][instrument]["lot"],
1176                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1177                ))
1178
1179        infoText = "".join(info)
1180
1181        if show:
1182            uLogger.info(infoText)
1183
1184        if self.instrumentsFile:
1185            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1186                fH.write(infoText)
1187
1188            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1189
1190        return infoText
1191
1192    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1193        """
1194        This method search and show information about instruments by part of its ticker, FIGI or name.
1195        If `searchResultsFile` string is not empty then also save information to this file.
1196
1197        :param pattern: string with part of ticker, FIGI or instrument's name.
1198        :param show: if `True` then print results to console, if `False` — return list of result only.
1199        :return: list of dictionaries with all found instruments.
1200        """
1201        if not self.iList:
1202            self.iList = self.Listing()
1203
1204        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1205        compiledPattern = re.compile(pattern, re.IGNORECASE)
1206
1207        for iType in self.iList:
1208            for instrument in self.iList[iType].values():
1209                searchResult = compiledPattern.search(" ".join(
1210                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1211                ))
1212
1213                if searchResult:
1214                    searchResults[iType][instrument["ticker"]] = instrument
1215
1216        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1217        info = [
1218            "# Search results\n\n",
1219            "* **Search pattern:** [{}]\n".format(pattern),
1220            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1221            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1222        ]
1223        infoShort = info[:]
1224
1225        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1226        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1227        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1228
1229        if resultsLen == 0:
1230            info.append("\nNo results\n")
1231            infoShort.append("\nNo results\n")
1232            uLogger.warning("No results. Try changing your search pattern.")
1233
1234        else:
1235            for iType in searchResults:
1236                iTypeValuesCount = len(searchResults[iType].values())
1237                if iTypeValuesCount > 0:
1238                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1239                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1240
1241                    for instrument in searchResults[iType].values():
1242                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1243                            instrument["type"],
1244                            instrument["ticker"],
1245                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1246                            instrument["figi"],
1247                        ))
1248
1249                    if iTypeValuesCount <= 5:
1250                        infoShort.extend(info[-iTypeValuesCount:])
1251
1252                    else:
1253                        infoShort.extend(info[-5:])
1254                        infoShort.append(skippedLine)
1255
1256        infoText = "".join(info)
1257        infoTextShort = "".join(infoShort)
1258
1259        if show:
1260            uLogger.info(infoTextShort)
1261            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1262
1263        if self.searchResultsFile:
1264            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1265                fH.write(infoText)
1266
1267            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1268
1269        return searchResults
1270
1271    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1272        """
1273        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1274
1275        :param instruments: list of strings with tickers or FIGIs.
1276        :return: list with unique instrument FIGIs only.
1277        """
1278        requestedInstruments = []
1279        for iName in instruments:
1280            if iName not in self.aliases.keys():
1281                if iName not in requestedInstruments:
1282                    requestedInstruments.append(iName)
1283
1284            else:
1285                if iName not in requestedInstruments:
1286                    if self.aliases[iName] not in requestedInstruments:
1287                        requestedInstruments.append(self.aliases[iName])
1288
1289        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1290
1291        onlyUniqueFIGIs = []
1292        for iName in requestedInstruments:
1293            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1294                continue
1295
1296            self.ticker = iName
1297            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1298
1299            if not iData:
1300                self.ticker = ""
1301                self.figi = iName
1302
1303                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1304
1305                if not iData:
1306                    self.figi = ""
1307                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1308
1309            if iData and iData["figi"] not in onlyUniqueFIGIs:
1310                onlyUniqueFIGIs.append(iData["figi"])
1311
1312        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1313
1314        return onlyUniqueFIGIs
1315
1316    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1317        """
1318        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1319
1320        See limits: https://tinkoff.github.io/investAPI/limits/
1321
1322        If `pricesFile` string is not empty then also save information to this file.
1323
1324        :param instruments: list of strings with tickers or FIGIs.
1325        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1326        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1327                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1328        """
1329        if instruments is None or not instruments:
1330            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1331            raise Exception("Ticker or FIGI required")
1332
1333        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1334
1335        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1336
1337        iList = []  # trying to get info and current prices about all unique instruments:
1338        for self.figi in onlyUniqueFIGIs:
1339            iData = self.SearchByFIGI(requestPrice=True)
1340            iList.append(iData)
1341
1342        self.ShowListOfPrices(iList, show)
1343
1344        return iList
1345
1346    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1347        """
1348        Show table contains current prices of given instruments.
1349
1350        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1351                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1352        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1353        :return: multilines text in Markdown format as a table contains current prices.
1354        """
1355        infoText = ""
1356
1357        if show or self.pricesFile:
1358            info = [
1359                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1360                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1361                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1362            ]
1363
1364            for item in iList:
1365                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1366                    item["ticker"],
1367                    item["figi"],
1368                    item["type"],
1369                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1370                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1371                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1372                    "{} / {}".format(
1373                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1374                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1375                    ),
1376                    "{} / {}".format(
1377                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1378                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1379                    ),
1380                    item["currency"],
1381                ))
1382
1383            infoText = "".join(info)
1384
1385            if show:
1386                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1387
1388            if self.pricesFile:
1389                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1390                    fH.write(infoText)
1391
1392                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1393
1394        return infoText
1395
1396    def RequestTradingStatus(self) -> dict:
1397        """
1398        Requesting trading status for the instrument defined by `figi` variable.
1399
1400        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1401
1402        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1403
1404        :return: dictionary with trading status attributes. Response example:
1405                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1406                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1407        """
1408        if self.figi is None or not self.figi:
1409            uLogger.error("Variable `figi` must be defined for using this method!")
1410            raise Exception("FIGI required")
1411
1412        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1413
1414        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1415        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1416        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1417
1418        if self.moreDebug:
1419            uLogger.debug("Records about current trading status successfully received")
1420
1421        return tradingStatus
1422
1423    def RequestPortfolio(self) -> dict:
1424        """
1425        Requesting actual user's portfolio for current `accountId`.
1426
1427        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1428
1429        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1430
1431        :return: dictionary with user's portfolio.
1432        """
1433        if self.accountId is None or not self.accountId:
1434            uLogger.error("Variable `accountId` must be defined for using this method!")
1435            raise Exception("Account ID required")
1436
1437        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1438
1439        self.body = str({"accountId": self.accountId})
1440        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1441        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1442
1443        if self.moreDebug:
1444            uLogger.debug("Records about user's portfolio successfully received")
1445
1446        return rawPortfolio
1447
1448    def RequestPositions(self) -> dict:
1449        """
1450        Requesting open positions by currencies and instruments for current `accountId`.
1451
1452        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1453
1454        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1455
1456        :return: dictionary with open positions by instruments.
1457        """
1458        if self.accountId is None or not self.accountId:
1459            uLogger.error("Variable `accountId` must be defined for using this method!")
1460            raise Exception("Account ID required")
1461
1462        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1463
1464        self.body = str({"accountId": self.accountId})
1465        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1466        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1467
1468        if self.moreDebug:
1469            uLogger.debug("Records about current open positions successfully received")
1470
1471        return rawPositions
1472
1473    def RequestPendingOrders(self) -> list:
1474        """
1475        Requesting current actual pending limit orders for current `accountId`.
1476
1477        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1478
1479        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1480
1481        :return: list of dictionaries with pending limit orders.
1482        """
1483        if self.accountId is None or not self.accountId:
1484            uLogger.error("Variable `accountId` must be defined for using this method!")
1485            raise Exception("Account ID required")
1486
1487        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1488
1489        self.body = str({"accountId": self.accountId})
1490        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1491        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1492
1493        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1494
1495        return rawOrders
1496
1497    def RequestStopOrders(self) -> list:
1498        """
1499        Requesting current actual stop orders for current `accountId`.
1500
1501        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1502
1503        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1504
1505        :return: list of dictionaries with stop orders.
1506        """
1507        if self.accountId is None or not self.accountId:
1508            uLogger.error("Variable `accountId` must be defined for using this method!")
1509            raise Exception("Account ID required")
1510
1511        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1512
1513        self.body = str({"accountId": self.accountId})
1514        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1515        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1516
1517        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1518
1519        return rawStopOrders
1520
1521    def Overview(self, show: bool = False, details: str = "full") -> dict:
1522        """
1523        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1524        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1525        and `overviewBondsCalendarFile` are defined then also save information to file.
1526
1527        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1528        many requests about the state of the portfolio, and then, based on the received data, a large number
1529        of calculation and statistics are collected.
1530
1531        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1532        :param details: how detailed should the information be?
1533        - `full` — shows full available information about portfolio status (by default),
1534        - `positions` — shows only open positions,
1535        - `orders` — shows only sections of open limits and stop orders.
1536        - `digest` — show a short digest of the portfolio status,
1537        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1538        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1539        :return: dictionary with client's raw portfolio and some statistics.
1540        """
1541        if self.accountId is None or not self.accountId:
1542            uLogger.error("Variable `accountId` must be defined for using this method!")
1543            raise Exception("Account ID required")
1544
1545        view = {
1546            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1547                "headers": {},  # list of dictionaries, response headers without "positions" section
1548                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1549                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1550                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1551                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1552                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1553                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1554                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1555                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1556                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1557            },
1558            "stat": {  # --- some statistics calculated using "raw" sections:
1559                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1560                "availableRUB": 0.,  # available rubles (without other currencies)
1561                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1562                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1563                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1564                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1565                "sharesCostRUB": 0.,  # costs of all shares in RUB
1566                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1567                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1568                "futuresCostRUB": 0.,  # costs of all futures in RUB
1569                "Currencies": [],  # list of dictionaries of all currencies statistics
1570                "Shares": [],  # list of dictionaries of all shares statistics
1571                "Bonds": [],  # list of dictionaries of all bonds statistics
1572                "Etfs": [],  # list of dictionaries of all etfs statistics
1573                "Futures": [],  # list of dictionaries of all futures statistics
1574                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1575                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1576                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1577                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1578                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1579            },
1580            "analytics": {  # --- some analytics of portfolio:
1581                "distrByAssets": {},  # portfolio distribution by assets
1582                "distrByCompanies": {},  # portfolio distribution by companies
1583                "distrBySectors": {},  # portfolio distribution by sectors
1584                "distrByCurrencies": {},  # portfolio distribution by currencies
1585                "distrByCountries": {},  # portfolio distribution by countries
1586                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1587            }
1588        }
1589
1590        details = details.lower()
1591        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1592        if details not in availableDetails:
1593            details = "full"
1594            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1595
1596        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1597
1598        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1599        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1600        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1601        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1602
1603        # save response headers without "positions" section:
1604        for key in portfolioResponse.keys():
1605            if key != "positions":
1606                view["raw"]["headers"][key] = portfolioResponse[key]
1607
1608            else:
1609                continue
1610
1611        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1612        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1613        for item in portfolioResponse["positions"]:
1614            if item["instrumentType"] == "currency":
1615                self.figi = item["figi"]
1616                curr = self.SearchByFIGI(requestPrice=False)
1617
1618                # current price of currency in RUB:
1619                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1620                    "name": curr["name"],
1621                    "currentPrice": NanoToFloat(
1622                        item["currentPrice"]["units"],
1623                        item["currentPrice"]["nano"]
1624                    ),
1625                }
1626
1627                view["raw"]["Currencies"].append(item)
1628
1629            elif item["instrumentType"] == "share":
1630                view["raw"]["Shares"].append(item)
1631
1632            elif item["instrumentType"] == "bond":
1633                view["raw"]["Bonds"].append(item)
1634
1635            elif item["instrumentType"] == "etf":
1636                view["raw"]["Etfs"].append(item)
1637
1638            elif item["instrumentType"] == "futures":
1639                view["raw"]["Futures"].append(item)
1640
1641            else:
1642                continue
1643
1644        # how many volume of currencies (by ISO currency name) are blocked:
1645        for item in view["raw"]["positions"]["blocked"]:
1646            blocked = NanoToFloat(item["units"], item["nano"])
1647            if blocked > 0:
1648                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1649
1650        # how many volume of instruments (by FIGI) are blocked:
1651        for item in view["raw"]["positions"]["securities"]:
1652            blocked = int(item["blocked"])
1653            if blocked > 0:
1654                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1655
1656        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1657
1658        if "rub" in allBlocked.keys():
1659            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1660
1661        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1662        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1663        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1664        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1665        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1666        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1667        view["stat"]["portfolioCostRUB"] = sum([
1668            view["stat"]["allCurrenciesCostRUB"],
1669            view["stat"]["sharesCostRUB"],
1670            view["stat"]["bondsCostRUB"],
1671            view["stat"]["etfsCostRUB"],
1672            view["stat"]["futuresCostRUB"],
1673        ])
1674
1675        # --- calculating some portfolio statistics:
1676        byComp = {}  # distribution by companies
1677        bySect = {}  # distribution by sectors
1678        byCurr = {}  # distribution by currencies (include RUB)
1679        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1680        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1681
1682        for item in portfolioResponse["positions"]:
1683            self.figi = item["figi"]
1684            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1685
1686            if instrument:
1687                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1688                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1689
1690                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1691                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1692
1693                else:
1694                    blocked = 0
1695
1696                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1697                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1698                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1699                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1700                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1701                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1702                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1703                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1704                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1705                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1706                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1707                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1708
1709                statData = {
1710                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1711                    "ticker": instrument["ticker"],  # ticker by FIGI
1712                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1713                    "volume": volume,  # available volume of instrument
1714                    "lots": lots,  # volume in lots of instrument
1715                    "direction": direction,  # direction of an instrument's position: short or long
1716                    "blocked": blocked,  # blocked volume of currency or instrument
1717                    "currentPrice": curPrice,  # current instrument's price in basic asset
1718                    "average": average,  # current average position price
1719                    "cost": cost,  # current cost of all volume of instrument in basic asset
1720                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1721                    "costRUB": costRUB,  # cost of instrument in ruble
1722                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1723                    "profit": profit,  # expected profit at current moment
1724                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1725                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1726                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1727                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1728                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1729                    "step": instrument["step"],  # minimum price increment
1730                }
1731
1732                # adding distribution by unique countries:
1733                if statData["country"] not in byCountry.keys():
1734                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1735
1736                else:
1737                    byCountry[statData["country"]]["cost"] += costRUB
1738                    byCountry[statData["country"]]["percent"] += percentCostRUB
1739
1740                if item["instrumentType"] != "currency":
1741                    # adding distribution by unique companies:
1742                    if statData["name"]:
1743                        if statData["name"] not in byComp.keys():
1744                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1745
1746                        else:
1747                            byComp[statData["name"]]["cost"] += costRUB
1748                            byComp[statData["name"]]["percent"] += percentCostRUB
1749
1750                    # adding distribution by unique sectors:
1751                    if statData["sector"] not in bySect.keys():
1752                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1753
1754                    else:
1755                        bySect[statData["sector"]]["cost"] += costRUB
1756                        bySect[statData["sector"]]["percent"] += percentCostRUB
1757
1758                # adding distribution by unique currencies:
1759                if currency not in byCurr.keys():
1760                    byCurr[currency] = {
1761                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1762                        "cost": costRUB,
1763                        "percent": percentCostRUB
1764                    }
1765
1766                else:
1767                    byCurr[currency]["cost"] += costRUB
1768                    byCurr[currency]["percent"] += percentCostRUB
1769
1770                # saving statistics for every instrument:
1771                if item["instrumentType"] == "currency":
1772                    view["stat"]["Currencies"].append(statData)
1773
1774                    # update dict with free funds for trading (total - blocked) by currencies
1775                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1776                    view["stat"]["funds"][currency] = {
1777                        "total": volume,
1778                        "totalCostRUB": costRUB,  # total volume cost in rubles
1779                        "free": volume - blocked,
1780                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1781                    }
1782
1783                elif item["instrumentType"] == "share":
1784                    view["stat"]["Shares"].append(statData)
1785
1786                elif item["instrumentType"] == "bond":
1787                    view["stat"]["Bonds"].append(statData)
1788
1789                elif item["instrumentType"] == "etf":
1790                    view["stat"]["Etfs"].append(statData)
1791
1792                elif item["instrumentType"] == "Futures":
1793                    view["stat"]["Futures"].append(statData)
1794
1795                else:
1796                    continue
1797
1798        # total changes in Russian Ruble:
1799        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1800        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1801        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1802        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1803        view["stat"]["funds"]["rub"] = {
1804            "total": view["stat"]["availableRUB"],
1805            "totalCostRUB": view["stat"]["availableRUB"],
1806            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1807            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1808        }
1809
1810        # --- pending limit orders sector data:
1811        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1812        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1813
1814        for item in view["raw"]["orders"]:
1815            self.figi = item["figi"]
1816
1817            if item["figi"] not in uniquePendingOrdersFIGIs:
1818                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1819
1820                uniquePendingOrdersFIGIs.append(item["figi"])
1821                uniquePendingOrders[item["figi"]] = instrument
1822
1823            else:
1824                instrument = uniquePendingOrders[item["figi"]]
1825
1826            if instrument:
1827                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1828                orderType = TKS_ORDER_TYPES[item["orderType"]]
1829                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1830                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1831
1832                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1833                if item["direction"] == "ORDER_DIRECTION_BUY":
1834                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1835
1836                else:
1837                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1838
1839                # requested price for order execution:
1840                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1841
1842                # necessary changes in percent to reach target from current price:
1843                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1844
1845                view["stat"]["orders"].append({
1846                    "orderID": item["orderId"],  # orderId number parameter of current order
1847                    "figi": item["figi"],  # FIGI identification
1848                    "ticker": instrument["ticker"],  # ticker name by FIGI
1849                    "lotsRequested": item["lotsRequested"],  # requested lots value
1850                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1851                    "currentPrice": lastPrice,  # current instrument's price for defined action
1852                    "targetPrice": target,  # requested price for order execution in base currency
1853                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1854                    "percentChanges": changes,  # changes in percent to target from current price
1855                    "currency": item["currency"],  # instrument's currency name
1856                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1857                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1858                    "status": orderState,  # order status from TKS_ORDER_STATES
1859                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1860                })
1861
1862        # --- stop orders sector data:
1863        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1864        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1865
1866        for item in view["raw"]["stopOrders"]:
1867            self.figi = item["figi"]
1868
1869            if item["figi"] not in uniqueStopOrdersFIGIs:
1870                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1871
1872                uniqueStopOrdersFIGIs.append(item["figi"])
1873                uniqueStopOrders[item["figi"]] = instrument
1874
1875            else:
1876                instrument = uniqueStopOrders[item["figi"]]
1877
1878            if instrument:
1879                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1880                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1881                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1882
1883                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1884                if "expirationTime" in item.keys():
1885                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1886                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1887
1888                else:
1889                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1890                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1891
1892                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1893                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1894                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1895
1896                else:
1897                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1898
1899                # requested price when stop-order executed:
1900                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1901
1902                # price for limit-order, set up when stop-order executed:
1903                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1904
1905                # necessary changes in percent to reach target from current price:
1906                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1907
1908                view["stat"]["stopOrders"].append({
1909                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1910                    "figi": item["figi"],  # FIGI identification
1911                    "ticker": instrument["ticker"],  # ticker name by FIGI
1912                    "lotsRequested": item["lotsRequested"],  # requested lots value
1913                    "currentPrice": lastPrice,  # current instrument's price for defined action
1914                    "targetPrice": target,  # requested price for stop-order execution in base currency
1915                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1916                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1917                    "percentChanges": changes,  # changes in percent to target from current price
1918                    "currency": item["currency"],  # instrument's currency name
1919                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1920                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1921                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1922                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1923                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1924                })
1925
1926        # --- calculating data for analytics section:
1927        # portfolio distribution by assets:
1928        view["analytics"]["distrByAssets"] = {
1929            "Ruble": {
1930                "uniques": 1,
1931                "cost": view["stat"]["availableRUB"],
1932                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1933            },
1934            "Currencies": {
1935                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1936                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1937                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1938            },
1939            "Shares": {
1940                "uniques": len(view["stat"]["Shares"]),
1941                "cost": view["stat"]["sharesCostRUB"],
1942                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1943            },
1944            "Bonds": {
1945                "uniques": len(view["stat"]["Bonds"]),
1946                "cost": view["stat"]["bondsCostRUB"],
1947                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1948            },
1949            "Etfs": {
1950                "uniques": len(view["stat"]["Etfs"]),
1951                "cost": view["stat"]["etfsCostRUB"],
1952                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1953            },
1954            "Futures": {
1955                "uniques": len(view["stat"]["Futures"]),
1956                "cost": view["stat"]["futuresCostRUB"],
1957                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1958            },
1959        }
1960
1961        # portfolio distribution by companies:
1962        view["analytics"]["distrByCompanies"]["All money cash"] = {
1963            "ticker": "",
1964            "cost": view["stat"]["allCurrenciesCostRUB"],
1965            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1966        }
1967        view["analytics"]["distrByCompanies"].update(byComp)
1968
1969        # portfolio distribution by sectors:
1970        view["analytics"]["distrBySectors"]["All money cash"] = {
1971            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1972            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1973        }
1974        view["analytics"]["distrBySectors"].update(bySect)
1975
1976        # portfolio distribution by currencies:
1977        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1978            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1979
1980            if self.moreDebug:
1981                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1982
1983        view["analytics"]["distrByCurrencies"].update(byCurr)
1984        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1985        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1986
1987        # portfolio distribution by countries:
1988        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1989            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1990
1991            if self.moreDebug:
1992                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1993
1994        view["analytics"]["distrByCountries"].update(byCountry)
1995        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1996        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1997
1998        # --- Prepare text statistics overview in human-readable:
1999        if show:
2000            # Whatever the value `details`, header not changes:
2001            info = [
2002                "# Client's portfolio\n\n",
2003                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2004                "* **Account ID:** [{}]\n".format(self.accountId),
2005            ]
2006
2007            if details in ["full", "positions", "digest"]:
2008                info.extend([
2009                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2010                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2011                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2012                        view["stat"]["totalChangesRUB"],
2013                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2014                        view["stat"]["totalChangesPercentRUB"],
2015                    ),
2016                ])
2017
2018            if details in ["full", "positions"]:
2019                info.extend([
2020                    "## Open positions\n\n",
2021                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2022                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2023                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2024                        "{:.2f} ({:.2f}) rub".format(
2025                            view["stat"]["availableRUB"],
2026                            view["stat"]["blockedRUB"],
2027                        )
2028                    )
2029                ])
2030
2031                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2032                    return [
2033                        "|                             |                                 |          |              |              |                     |                              |\n",
2034                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2035                            noTradeStr if noTradeStr else typeStr,
2036                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2037                        ),
2038                    ]
2039
2040                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2041                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2042                        "{} [{}]".format(data["ticker"], data["figi"]),
2043                        "{:.2f} ({:.2f}) {}".format(
2044                            data["volume"],
2045                            data["blocked"],
2046                            data["currency"],
2047                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2048                            data["volume"],
2049                            data["blocked"],
2050                        ),
2051                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2052                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2053                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2054                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2055                        "{}{:.2f} {} ({}{:.2f}%)".format(
2056                            "+" if data["profit"] > 0 else "",
2057                            data["profit"], data["baseCurrencyName"],
2058                            "+" if data["percentProfit"] > 0 else "",
2059                            data["percentProfit"],
2060                        ),
2061                    )
2062
2063                # --- Show currencies section:
2064                if view["stat"]["Currencies"]:
2065                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2066                    for item in view["stat"]["Currencies"]:
2067                        info.append(_InfoStr(item, showCurrencyName=True))
2068
2069                else:
2070                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2071
2072                # --- Show shares section:
2073                if view["stat"]["Shares"]:
2074                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2075
2076                    for item in view["stat"]["Shares"]:
2077                        info.append(_InfoStr(item))
2078
2079                else:
2080                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2081
2082                # --- Show bonds section:
2083                if view["stat"]["Bonds"]:
2084                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2085
2086                    for item in view["stat"]["Bonds"]:
2087                        info.append(_InfoStr(item))
2088
2089                else:
2090                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2091
2092                # --- Show etfs section:
2093                if view["stat"]["Etfs"]:
2094                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2095
2096                    for item in view["stat"]["Etfs"]:
2097                        info.append(_InfoStr(item))
2098
2099                else:
2100                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2101
2102                # --- Show futures section:
2103                if view["stat"]["Futures"]:
2104                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2105
2106                    for item in view["stat"]["Futures"]:
2107                        info.append(_InfoStr(item))
2108
2109                else:
2110                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2111
2112            if details in ["full", "orders"]:
2113                # --- Show pending limit orders section:
2114                if view["stat"]["orders"]:
2115                    info.extend([
2116                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2117                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2118                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2119                    ])
2120
2121                    for item in view["stat"]["orders"]:
2122                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2123                            "{} [{}]".format(item["ticker"], item["figi"]),
2124                            item["orderID"],
2125                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2126                            "{} {} ({}{:.2f}%)".format(
2127                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2128                                item["baseCurrencyName"],
2129                                "+" if item["percentChanges"] > 0 else "",
2130                                float(item["percentChanges"]),
2131                            ),
2132                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2133                            item["action"],
2134                            item["type"],
2135                            item["date"],
2136                        ))
2137
2138                else:
2139                    info.append("\n## Total pending limit-orders: 0\n")
2140
2141                # --- Show stop orders section:
2142                if view["stat"]["stopOrders"]:
2143                    info.extend([
2144                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2145                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2146                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2147                    ])
2148
2149                    for item in view["stat"]["stopOrders"]:
2150                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2151                            "{} [{}]".format(item["ticker"], item["figi"]),
2152                            item["orderID"],
2153                            item["lotsRequested"],
2154                            "{} {} ({}{:.2f}%)".format(
2155                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2156                                item["baseCurrencyName"],
2157                                "+" if item["percentChanges"] > 0 else "",
2158                                float(item["percentChanges"]),
2159                            ),
2160                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2161                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2162                            item["action"],
2163                            item["type"],
2164                            item["expType"],
2165                            item["createDate"],
2166                            item["expDate"],
2167                        ))
2168
2169                else:
2170                    info.append("\n## Total stop-orders: 0\n")
2171
2172            if details in ["full", "analytics"]:
2173                # -- Show analytics section:
2174                if view["stat"]["portfolioCostRUB"] > 0:
2175                    info.extend([
2176                        "\n# Analytics\n"
2177                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2178                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2179                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2180                            view["stat"]["totalChangesRUB"],
2181                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2182                            view["stat"]["totalChangesPercentRUB"],
2183                        ),
2184                        "\n## Portfolio distribution by assets\n"
2185                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2186                        "|------------------------------------|---------|---------|--------------------|\n",
2187                    ])
2188
2189                    for key in view["analytics"]["distrByAssets"].keys():
2190                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2191                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2192                                key,
2193                                view["analytics"]["distrByAssets"][key]["uniques"],
2194                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2195                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2196                            ))
2197
2198                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2199
2200                    info.extend([
2201                        "\n## Portfolio distribution by companies\n"
2202                        "\n| Company                                      | Percent | Current cost       |\n",
2203                        aSepLine,
2204                    ])
2205
2206                    for company in view["analytics"]["distrByCompanies"].keys():
2207                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2208                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2209                                "{}{}".format(
2210                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2211                                    company,
2212                                ),
2213                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2214                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2215                            ))
2216
2217                    info.extend([
2218                        "\n## Portfolio distribution by sectors\n"
2219                        "\n| Sector                                       | Percent | Current cost       |\n",
2220                        aSepLine,
2221                    ])
2222
2223                    for sector in view["analytics"]["distrBySectors"].keys():
2224                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2225                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2226                                sector,
2227                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2228                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2229                            ))
2230
2231                    info.extend([
2232                        "\n## Portfolio distribution by currencies\n"
2233                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2234                        aSepLine,
2235                    ])
2236
2237                    for curr in view["analytics"]["distrByCurrencies"].keys():
2238                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2239                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2240                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2241                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2242                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2243                            ))
2244
2245                    info.extend([
2246                        "\n## Portfolio distribution by countries\n"
2247                        "\n| Assets by country                            | Percent | Current cost       |\n",
2248                        aSepLine,
2249                    ])
2250
2251                    for country in view["analytics"]["distrByCountries"].keys():
2252                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2253                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2254                                country,
2255                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2256                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2257                            ))
2258
2259            if details in ["full", "calendar"]:
2260                # -- Show bonds payment calendar section:
2261                if view["stat"]["Bonds"]:
2262                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2263                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2264                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2265
2266                else:
2267                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2268
2269            infoText = "".join(info)
2270
2271            uLogger.info(infoText)
2272
2273            if details == "full" and self.overviewFile:
2274                filename = self.overviewFile
2275
2276            elif details == "digest" and self.overviewDigestFile:
2277                filename = self.overviewDigestFile
2278
2279            elif details == "positions" and self.overviewPositionsFile:
2280                filename = self.overviewPositionsFile
2281
2282            elif details == "orders" and self.overviewOrdersFile:
2283                filename = self.overviewOrdersFile
2284
2285            elif details == "analytics" and self.overviewAnalyticsFile:
2286                filename = self.overviewAnalyticsFile
2287
2288            elif details == "calendar" and self.overviewBondsCalendarFile:
2289                filename = self.overviewBondsCalendarFile
2290
2291            else:
2292                filename = ""
2293
2294            if filename:
2295                with open(filename, "w", encoding="UTF-8") as fH:
2296                    fH.write(infoText)
2297
2298                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2299
2300        return view
2301
2302    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2303        """
2304        Returns history operations between two given dates for current `accountId`.
2305        If `reportFile` string is not empty then also save human-readable report.
2306        Shows some statistical data of closed positions.
2307
2308        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2309        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2310        :param show: if `True` then also prints all records to the console.
2311        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2312        :return: original list of dictionaries with history of deals records from API ("operations" key):
2313                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2314                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2315        """
2316        if self.accountId is None or not self.accountId:
2317            uLogger.error("Variable `accountId` must be defined for using this method!")
2318            raise Exception("Account ID required")
2319
2320        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2321
2322        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2323
2324        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2325        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2326        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2327        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2328        customStat = {}  # custom statistics in additional to responseJSON
2329
2330        # --- output report in human-readable format:
2331        if show or self.reportFile:
2332            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2333            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2334            nextDay = ""
2335
2336            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2337
2338            if len(ops) > 0:
2339                customStat = {
2340                    "opsCount": 0,  # total operations count
2341                    "buyCount": 0,  # buy operations
2342                    "sellCount": 0,  # sell operations
2343                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2344                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2345                    "payIn": {"rub": 0.},  # Deposit brokerage account
2346                    "payOut": {"rub": 0.},  # Withdrawals
2347                    "divs": {"rub": 0.},  # Dividends income
2348                    "coupons": {"rub": 0.},  # Coupon's income
2349                    "brokerCom": {"rub": 0.},  # Service commissions
2350                    "serviceCom": {"rub": 0.},  # Service commissions
2351                    "marginCom": {"rub": 0.},  # Margin commissions
2352                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2353                }
2354
2355                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2356                for item in ops:
2357                    if item["state"] == "OPERATION_STATE_EXECUTED":
2358                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2359
2360                        # count buy operations:
2361                        if "_BUY" in item["operationType"]:
2362                            customStat["buyCount"] += 1
2363
2364                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2365                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2366
2367                            else:
2368                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2369
2370                        # count sell operations:
2371                        elif "_SELL" in item["operationType"]:
2372                            customStat["sellCount"] += 1
2373
2374                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2375                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2376
2377                            else:
2378                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2379
2380                        # count incoming operations:
2381                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2382                            if item["payment"]["currency"] in customStat["payIn"].keys():
2383                                customStat["payIn"][item["payment"]["currency"]] += payment
2384
2385                            else:
2386                                customStat["payIn"][item["payment"]["currency"]] = payment
2387
2388                        # count withdrawals operations:
2389                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2390                            if item["payment"]["currency"] in customStat["payOut"].keys():
2391                                customStat["payOut"][item["payment"]["currency"]] += payment
2392
2393                            else:
2394                                customStat["payOut"][item["payment"]["currency"]] = payment
2395
2396                        # count dividends income:
2397                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2398                            if item["payment"]["currency"] in customStat["divs"].keys():
2399                                customStat["divs"][item["payment"]["currency"]] += payment
2400
2401                            else:
2402                                customStat["divs"][item["payment"]["currency"]] = payment
2403
2404                        # count coupon's income:
2405                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2406                            if item["payment"]["currency"] in customStat["coupons"].keys():
2407                                customStat["coupons"][item["payment"]["currency"]] += payment
2408
2409                            else:
2410                                customStat["coupons"][item["payment"]["currency"]] = payment
2411
2412                        # count broker commissions:
2413                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2414                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2415                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2416
2417                            else:
2418                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2419
2420                        # count service commissions:
2421                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2422                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2423                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2424
2425                            else:
2426                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2427
2428                        # count margin commissions:
2429                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2430                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2431                                customStat["marginCom"][item["payment"]["currency"]] += payment
2432
2433                            else:
2434                                customStat["marginCom"][item["payment"]["currency"]] = payment
2435
2436                        # count withholding taxes:
2437                        elif "_TAX" in item["operationType"]:
2438                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2439                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2440
2441                            else:
2442                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2443
2444                        else:
2445                            continue
2446
2447                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2448
2449                # --- view "Actions" lines:
2450                info.extend([
2451                    "| Report sections            |                               |                              |                      |                        |\n",
2452                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2453                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2454                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2455                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2456                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2457                    ),
2458                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2459                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2460                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2461                    ),
2462                ])
2463
2464                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2465                for key in opsKeys:
2466                    if key == "rub":
2467                        continue
2468
2469                    info.extend([
2470                        "|                            |                               | {:<28} |                      |                        |\n".format(
2471                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2472                        ),
2473                        "|                            |                               | {:<28} |                      |                        |\n".format(
2474                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2475                        ),
2476                    ])
2477
2478                info.append(splitLine1)
2479
2480                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2481                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2482                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2483                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2484                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2485                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2486                    )
2487
2488                # --- view "Payments" lines:
2489                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2490                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2491
2492                for key in paymentsKeys:
2493                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2494
2495                info.append(splitLine1)
2496
2497                # --- view "Commissions and taxes" lines:
2498                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2499                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2500
2501                for key in comKeys:
2502                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2503
2504                info.append(splitLine1)
2505
2506                info.extend([
2507                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2508                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2509                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2510                ])
2511
2512            else:
2513                info.append("Broker returned no operations during this period\n")
2514
2515            # --- view "Operations" section:
2516            for item in ops:
2517                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2518                    continue
2519
2520                else:
2521                    self.figi = item["figi"] if item["figi"] else ""
2522                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2523                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2524
2525                    # group of deals during one day:
2526                    if nextDay and item["date"].split("T")[0] != nextDay:
2527                        info.append(splitLine2)
2528                        nextDay = ""
2529
2530                    else:
2531                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2532
2533                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2534                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2535                        self.figi if self.figi else "—",
2536                        instrument["ticker"] if instrument else "—",
2537                        instrument["type"] if instrument else "—",
2538                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2539                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2540                        TKS_OPERATION_STATES[item["state"]],
2541                        TKS_OPERATION_TYPES[item["operationType"]],
2542                    ))
2543
2544            infoText = "".join(info)
2545
2546            if show:
2547                if self.moreDebug:
2548                    uLogger.debug("Records about history of a client's operations successfully received")
2549
2550                uLogger.info(infoText)
2551
2552            if self.reportFile:
2553                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2554                    fH.write(infoText)
2555
2556                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2557
2558        return ops, customStat
2559
2560    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2561        """
2562        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2563
2564        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2565        Warning! Broker server used ISO UTC time by default.
2566
2567        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2568        Also, `historyFile` used to update history with `onlyMissing` parameter.
2569
2570        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2571
2572        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2573        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2574        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2575                         `"hour"`, `"day"`. Default: `"hour"`.
2576        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2577                            False by default. Warning! History appends only from last candle to current time
2578                            with always update last candle!
2579        :param csvSep: separator if csv-file is used, `,` by default.
2580        :param show: if `True` then also prints Pandas DataFrame to the console.
2581        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2582                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2583        """
2584        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2585        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2586        history = None  # empty pandas object for history
2587
2588        if interval not in TKS_CANDLE_INTERVALS.keys():
2589            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2590            raise Exception("Incorrect value")
2591
2592        if not (self.ticker or self.figi):
2593            uLogger.error("Ticker or FIGI must be defined!")
2594            raise Exception("Ticker or FIGI required")
2595
2596        if self.ticker and not self.figi:
2597            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2598            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2599
2600        if self.figi and not self.ticker:
2601            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2602            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2603
2604        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2605        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2606        if interval.lower() != "day":
2607            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2608
2609        delta = dtEnd - dtStart  # current UTC time minus last time in file
2610        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2611
2612        # calculate history length in candles:
2613        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2614        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2615            length += 1  # to avoid fraction time
2616
2617        # calculate data blocks count:
2618        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2619
2620        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2621        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2622        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2623        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2624        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2625
2626        tempOld = None  # pandas object for old history, if --only-missing key present
2627        lastTime = None  # datetime object of last old candle in file
2628
2629        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2630            uLogger.debug("--only-missing key present, add only last missing candles...")
2631            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2632
2633            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2634
2635            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2636            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2637            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2638            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2639
2640            # get last datetime object from last string in file or minus 1 delta if file is empty:
2641            if len(tempOld) > 0:
2642                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2643
2644            else:
2645                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2646
2647            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2648
2649        responseJSONs = []  # raw history blocks of data
2650
2651        blockEnd = dtEnd
2652        for item in range(blocks):
2653            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2654            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2655
2656            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2657                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2658            ))
2659
2660            if blockStart == blockEnd:
2661                uLogger.debug("Skipped this zero-length block...")
2662
2663            else:
2664                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2665                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2666                self.body = str({
2667                    "figi": self.figi,
2668                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2669                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2670                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2671                })
2672                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2673
2674                if "code" in responseJSON.keys():
2675                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2676
2677                else:
2678                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2679                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2680
2681                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2682
2683            blockEnd = blockStart
2684
2685        printCount = len(responseJSONs)  # candles to show in console
2686        if responseJSONs:
2687            tempHistory = pd.DataFrame(
2688                data={
2689                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2690                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2691                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2692                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2693                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2694                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2695                    "volume": [int(item["volume"]) for item in responseJSONs],
2696                },
2697                index=range(len(responseJSONs)),
2698                columns=["date", "time", "open", "high", "low", "close", "volume"],
2699            )
2700            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2701            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2702
2703            # append only newest candles to old history if --only-missing key present:
2704            if onlyMissing and tempOld is not None and lastTime is not None:
2705                index = 0  # find start index in tempHistory data:
2706
2707                for i, item in tempHistory.iterrows():
2708                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2709
2710                    if curTime == lastTime:
2711                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2712                        index = i
2713                        printCount = index + 1
2714                        break
2715
2716                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2717
2718            else:
2719                history = tempHistory  # if no `--only-missing` key then load full data from server
2720
2721            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2722
2723        if history is not None and not history.empty:
2724            if show:
2725                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2726                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2727                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2728                ))
2729
2730        else:
2731            uLogger.warning("Received an empty candles history!")
2732
2733        if self.historyFile is not None:
2734            if history is not None and not history.empty:
2735                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2736                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2737
2738            else:
2739                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2740
2741        else:
2742            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2743
2744        return history
2745
2746    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2747        """
2748        Load candles history from csv-file and return Pandas DataFrame object.
2749
2750        See also: `History()` and `ShowHistoryChart()` methods.
2751
2752        :param filePath: path to csv-file to open.
2753        """
2754        loadedHistory = None  # init candles data object
2755
2756        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2757
2758        if os.path.exists(filePath):
2759            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2760
2761            tfStr = self.priceModel.FormattedDelta(
2762                self.priceModel.timeframe,
2763                "{days} days {hours}h {minutes}m {seconds}s",
2764            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2765                self.priceModel.timeframe,
2766                "{hours}h {minutes}m {seconds}s",
2767            )
2768
2769            if loadedHistory is not None and not loadedHistory.empty:
2770                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2771                    len(loadedHistory),
2772                    tfStr,
2773                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2774                )
2775
2776            else:
2777                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2778
2779        else:
2780            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2781
2782        return loadedHistory
2783
2784    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2785        """
2786        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2787
2788        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2789        Default: `index.html` (both for interact and non-interact candlesticks chart).
2790
2791        See also: `History()` and `LoadHistory()` methods.
2792
2793        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2794        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2795                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2796                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2797                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2798        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2799                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2800        """
2801        if isinstance(candles, str):
2802            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2803            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2804
2805        elif isinstance(candles, pd.DataFrame):
2806            self.priceModel.prices = candles  # set candles chain from variable
2807            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2808
2809            if "datetime" not in candles.columns:
2810                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2811
2812        else:
2813            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2814            raise Exception("Incorrect value")
2815
2816        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2817
2818        if interact:
2819            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2820
2821            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2822
2823        else:
2824            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2825
2826            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2827
2828        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2829
2830    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2831        """
2832        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2833        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2834
2835        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2836
2837        :param operation: string "Buy" or "Sell".
2838        :param lots: volume, integer count of lots >= 1.
2839        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2840        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2841        :param expDate: string "Undefined" by default or local date in future,
2842                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2843        :return: JSON with response from broker server.
2844        """
2845        if self.accountId is None or not self.accountId:
2846            uLogger.error("Variable `accountId` must be defined for using this method!")
2847            raise Exception("Account ID required")
2848
2849        if operation is None or not operation or operation not in ("Buy", "Sell"):
2850            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2851            raise Exception("Incorrect value")
2852
2853        if lots is None or lots < 1:
2854            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2855            lots = 1
2856
2857        if tp is None or tp < 0:
2858            tp = 0
2859
2860        if sl is None or sl < 0:
2861            sl = 0
2862
2863        if expDate is None or not expDate:
2864            expDate = "Undefined"
2865
2866        if not (self.ticker or self.figi):
2867            uLogger.error("Ticker or FIGI must be defined!")
2868            raise Exception("Ticker or FIGI required")
2869
2870        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2871        self.ticker = instrument["ticker"]
2872        self.figi = instrument["figi"]
2873
2874        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2875
2876        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2877        self.body = str({
2878            "figi": self.figi,
2879            "quantity": str(lots),
2880            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2881            "accountId": str(self.accountId),
2882            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2883        })
2884        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2885
2886        if "orderId" in response.keys():
2887            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2888                operation, response["orderId"],
2889                self.ticker, self.figi, lots,
2890                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2891                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2892                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2893            ))
2894
2895            if tp > 0:
2896                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2897
2898            if sl > 0:
2899                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2900
2901        else:
2902            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2903
2904        return response
2905
2906    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2907        """
2908        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2909        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2910
2911        See also: `Order()` and `Trade()` docstrings.
2912
2913        :param lots: volume, integer count of lots >= 1.
2914        :param tp: float > 0, take profit price of stop-order.
2915        :param sl: float > 0, stop loss price of stop-order.
2916        :param expDate: it's a local date in future.
2917                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2918        :return: JSON with response from broker server.
2919        """
2920        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2921
2922    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2923        """
2924        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2925        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2926
2927        See also: `Order()` and `Trade()` docstrings.
2928
2929        :param lots: volume, integer count of lots >= 1.
2930        :param tp: float > 0, take profit price of stop-order.
2931        :param sl: float > 0, stop loss price of stop-order.
2932        :param expDate: it's a local date in the future.
2933                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2934        :return: JSON with response from broker server.
2935        """
2936        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2937
2938    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2939        """
2940        Close position of given instruments.
2941
2942        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2943        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2944                         This avoids unnecessary downloading data from the server.
2945        """
2946        if instruments is None or not instruments:
2947            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2948            raise Exception("Ticker or FIGI required")
2949
2950        if isinstance(instruments, str):
2951            instruments = [instruments]
2952
2953        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2954        if uniqueInstruments:
2955            if portfolio is None or not portfolio:
2956                portfolio = self.Overview(show=False)
2957
2958            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2959            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2960
2961            for self.figi in uniqueInstruments:
2962                if self.figi not in allOpened:
2963                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2964                    continue
2965
2966                # search open trade info about instrument by ticker:
2967                instrument = {}
2968                for iType in TKS_INSTRUMENTS:
2969                    if instrument:
2970                        break
2971
2972                    for item in portfolio["stat"][iType]:
2973                        if item["figi"] == self.figi:
2974                            instrument = item
2975                            break
2976
2977                if instrument:
2978                    self.ticker = instrument["ticker"]
2979                    self.figi = instrument["figi"]
2980
2981                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2982                        self.ticker,
2983                        self.figi,
2984                        int(instrument["volume"]),
2985                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2986                    ))
2987
2988                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2989
2990                    if tradeLots > 0:
2991                        if instrument["blocked"] > 0:
2992                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2993                                instrument["blocked"],
2994                                self.ticker,
2995                                tradeLots,
2996                            ))
2997
2998                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2999                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3000
3001                    else:
3002                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3003
3004    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3005        """
3006        Close all positions of given instruments with defined type.
3007
3008        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3009        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3010                         This avoids unnecessary downloading data from the server.
3011        """
3012        if iType not in TKS_INSTRUMENTS:
3013            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3014
3015        else:
3016            if portfolio is None or not portfolio:
3017                portfolio = self.Overview(show=False)
3018
3019            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3020            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3021
3022            if tickers and portfolio:
3023                self.CloseTrades(tickers, portfolio)
3024
3025            else:
3026                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3027
3028    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3029        """
3030        Universal method to create market or limit orders with all available parameters for current `accountId`.
3031        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3032
3033        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3034        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3035
3036        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3037        then broker immediately open market order as you can do simple --buy or --sell operations!
3038
3039        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3040        When current price will go up or down to target price value then broker opens a limit order.
3041        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3042
3043        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3044
3045        :param operation: string "Buy" or "Sell".
3046        :param orderType: string "Limit" or "Stop".
3047        :param lots: volume, integer count of lots >= 1.
3048        :param targetPrice: target price > 0. This is open trade price for limit order.
3049        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3050                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3051        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3052                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3053                         Stop loss order always executed by market price.
3054        :param expDate: string "Undefined" by default or local date in future.
3055                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3056                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3057                        A limit order has no expiration date, it lasts until the end of the trading day.
3058        :return: JSON with response from broker server.
3059        """
3060        if self.accountId is None or not self.accountId:
3061            uLogger.error("Variable `accountId` must be defined for using this method!")
3062            raise Exception("Account ID required")
3063
3064        if operation is None or not operation or operation not in ("Buy", "Sell"):
3065            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3066            raise Exception("Incorrect value")
3067
3068        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3069            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3070            raise Exception("Incorrect value")
3071
3072        if lots is None or lots < 1:
3073            uLogger.error("You must define trade volume > 0: integer count of lots!")
3074            raise Exception("Incorrect value")
3075
3076        if targetPrice is None or targetPrice <= 0:
3077            uLogger.error("Target price for limit-order must be greater than 0!")
3078            raise Exception("Incorrect value")
3079
3080        if limitPrice is None or limitPrice <= 0:
3081            limitPrice = targetPrice
3082
3083        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3084            stopType = "Limit"
3085
3086        if expDate is None or not expDate:
3087            expDate = "Undefined"
3088
3089        if not (self.ticker or self.figi):
3090            uLogger.error("Tocker or FIGI must be defined!")
3091            raise Exception("Ticker or FIGI required")
3092
3093        response = {}
3094        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3095        self.ticker = instrument["ticker"]
3096        self.figi = instrument["figi"]
3097
3098        if orderType == "Limit":
3099            uLogger.debug(
3100                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3101                    self.ticker, self.figi,
3102                    operation, lots, targetPrice, instrument["currency"],
3103                ))
3104
3105            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3106            self.body = str({
3107                "figi": self.figi,
3108                "quantity": str(lots),
3109                "price": FloatToNano(targetPrice),
3110                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3111                "accountId": str(self.accountId),
3112                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3113            })
3114            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3115
3116            if "orderId" in response.keys():
3117                uLogger.info(
3118                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3119                        response["orderId"],
3120                        self.ticker, self.figi,
3121                        operation, lots, targetPrice, instrument["currency"],
3122                    ))
3123
3124                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3125                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3126                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3127                            targetPrice, instrument["currency"],
3128                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3129                        ))
3130
3131                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3132                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3133                            targetPrice, instrument["currency"],
3134                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3135                        ))
3136
3137            else:
3138                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3139
3140        if orderType == "Stop":
3141            uLogger.debug(
3142                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3143                    self.ticker, self.figi,
3144                    operation, lots,
3145                    targetPrice, instrument["currency"],
3146                    limitPrice, instrument["currency"],
3147                    stopType, expDate,
3148                ))
3149
3150            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3151            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3152            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3153
3154            body = {
3155                "figi": self.figi,
3156                "quantity": str(lots),
3157                "price": FloatToNano(limitPrice),
3158                "stopPrice": FloatToNano(targetPrice),
3159                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3160                "accountId": str(self.accountId),
3161                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3162                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3163            }
3164
3165            if expDateUTC:
3166                body["expireDate"] = expDateUTC
3167
3168            self.body = str(body)
3169            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3170
3171            if "stopOrderId" in response.keys():
3172                uLogger.info(
3173                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3174                        response["stopOrderId"],
3175                        self.ticker, self.figi,
3176                        operation, lots,
3177                        targetPrice, instrument["currency"],
3178                        limitPrice, instrument["currency"],
3179                        TKS_STOP_ORDER_TYPES[stopOrderType],
3180                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3181                    ))
3182
3183                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3184                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3185                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3186                            targetPrice, instrument["currency"],
3187                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3188                        ))
3189
3190                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3191                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3192                            targetPrice, instrument["currency"],
3193                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3194                        ))
3195
3196            else:
3197                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3198
3199        return response
3200
3201    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3202        """
3203        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3204        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3205        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3206        See also: `Order()` docstring.
3207
3208        :param lots: volume, integer count of lots >= 1.
3209        :param targetPrice: target price > 0. This is open trade price for limit order.
3210        :return: JSON with response from broker server.
3211        """
3212        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3213
3214    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3215        """
3216        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3217        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3218        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3219        target price value then broker opens a limit order. See also: `Order()` docstring.
3220
3221        :param lots: volume, integer count of lots >= 1.
3222        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3223        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3224                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3225        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3226                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3227        :param expDate: string "Undefined" by default or local date in future.
3228                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3229                        This date is converting to UTC format for server.
3230        :return: JSON with response from broker server.
3231        """
3232        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3233
3234    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3235        """
3236        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3237        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3238        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3239        See also: `Order()` docstring.
3240
3241        :param lots: volume, integer count of lots >= 1.
3242        :param targetPrice: target price > 0. This is open trade price for limit order.
3243        :return: JSON with response from broker server.
3244        """
3245        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3246
3247    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3248        """
3249        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3250        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3251        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3252        target price value then broker opens a limit order. See also: `Order()` docstring.
3253
3254        :param lots: volume, integer count of lots >= 1.
3255        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3256        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3257                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3258        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3259                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3260        :param expDate: string "Undefined" by default or local date in future.
3261                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3262                        This date is converting to UTC format for server.
3263        :return: JSON with response from broker server.
3264        """
3265        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3266
3267    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3268        """
3269        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3270
3271        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3272        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3273                             This avoids unnecessary downloading data from the server.
3274        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3275        """
3276        if self.accountId is None or not self.accountId:
3277            uLogger.error("Variable `accountId` must be defined for using this method!")
3278            raise Exception("Account ID required")
3279
3280        if orderIDs:
3281            if allOrdersIDs is None:
3282                rawOrders = self.RequestPendingOrders()
3283                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3284
3285            if allStopOrdersIDs is None:
3286                rawStopOrders = self.RequestStopOrders()
3287                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3288
3289            for orderID in orderIDs:
3290                idInPendingOrders = orderID in allOrdersIDs
3291                idInStopOrders = orderID in allStopOrdersIDs
3292
3293                if not (idInPendingOrders or idInStopOrders):
3294                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3295                    continue
3296
3297                else:
3298                    if idInPendingOrders:
3299                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3300
3301                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3302                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3303                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3304                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3305
3306                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3307                            if self.moreDebug:
3308                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3309
3310                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3311
3312                        else:
3313                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3314
3315                    elif idInStopOrders:
3316                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3317
3318                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3319                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3320                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3321                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3322
3323                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3324                            if self.moreDebug:
3325                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3326
3327                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3328
3329                        else:
3330                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3331
3332                    else:
3333                        continue
3334
3335    def CloseAllOrders(self) -> None:
3336        """
3337        Gets a list of open pending and stop orders and cancel it all.
3338        """
3339        rawOrders = self.RequestPendingOrders()
3340        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3341        lenOrders = len(allOrdersIDs)
3342
3343        rawStopOrders = self.RequestStopOrders()
3344        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3345        lenSOrders = len(allStopOrdersIDs)
3346
3347        if lenOrders > 0 or lenSOrders > 0:
3348            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3349
3350            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3351
3352        else:
3353            uLogger.info("Orders not found, nothing to cancel.")
3354
3355    def CloseAll(self, *args) -> None:
3356        """
3357        Close all available (not blocked) opened trades and orders.
3358
3359        Also, you can select one or more keywords case-insensitive:
3360        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3361
3362        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3363        """
3364        overview = self.Overview(show=False)  # get all open trades info
3365
3366        if len(args) == 0:
3367            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3368            self.CloseAllOrders()  # close all pending and stop orders
3369
3370            for iType in TKS_INSTRUMENTS:
3371                if iType != "Currencies":
3372                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3373
3374        else:
3375            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3376            lowerArgs = [x.lower() for x in args]
3377
3378            if "orders" in lowerArgs:
3379                self.CloseAllOrders()  # close all pending and stop orders
3380
3381            for iType in TKS_INSTRUMENTS:
3382                if iType.lower() in lowerArgs and iType != "Currencies":
3383                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3384
3385    def CloseAllByTicker(self, instrument: str) -> None:
3386        """
3387        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3388
3389        This method searches opened trade and orders of instrument throw all portfolio and then use
3390        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3391
3392        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3393
3394        :param instrument: string with ticker.
3395        """
3396        if instrument is None or not instrument:
3397            uLogger.error("Ticker name must be defined for using this method!")
3398            raise Exception("Ticker required")
3399
3400        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3401
3402        self.ticker = instrument  # try to set instrument as ticker
3403        self.figi = ""
3404
3405        if self.IsInPortfolio(portfolio=overview):
3406            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3407            self.CloseTrades(instruments=[instrument], portfolio=overview)
3408
3409        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3410        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3411
3412        if limitAll and self.IsInLimitOrders(portfolio=overview):
3413            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3414            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3415
3416        if stopAll and self.IsInStopOrders(portfolio=overview):
3417            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3418            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3419
3420    def CloseAllByFIGI(self, instrument: str) -> None:
3421        """
3422        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3423
3424        This method searches opened trade and orders of instrument throw all portfolio and then use
3425        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3426
3427        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3428
3429        :param instrument: string with FIGI id.
3430        """
3431        if instrument is None or not instrument:
3432            uLogger.error("FIGI id must be defined for using this method!")
3433            raise Exception("FIGI required")
3434
3435        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3436
3437        self.ticker = ""
3438        self.figi = instrument  # try to set instrument as FIGI id
3439
3440        if self.IsInPortfolio(portfolio=overview):
3441            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3442            self.CloseTrades(instruments=[instrument], portfolio=overview)
3443
3444        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3445        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3446
3447        if limitAll and self.IsInLimitOrders(portfolio=overview):
3448            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3449            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3450
3451        if stopAll and self.IsInStopOrders(portfolio=overview):
3452            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3453            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3454
3455    @staticmethod
3456    def ParseOrderParameters(operation, **inputParameters):
3457        """
3458        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3459
3460        :param operation: string "Buy" or "Sell".
3461        :param inputParameters: this is dict of strings that looks like this
3462               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3463               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3464               "prices" key: one or more prices to open limit-orders
3465               Counts of values in lots and prices lists must be equals!
3466        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3467        """
3468        # TODO: update order grid work with api v2
3469        pass
3470        # uLogger.debug("Input parameters: {}".format(inputParameters))
3471        #
3472        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3473        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3474        #     raise Exception("Incorrect value")
3475        #
3476        # if "l" in inputParameters.keys():
3477        #     inputParameters["lots"] = inputParameters.pop("l")
3478        #
3479        # if "p" in inputParameters.keys():
3480        #     inputParameters["prices"] = inputParameters.pop("p")
3481        #
3482        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3483        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3484        #     raise Exception("Incorrect value")
3485        #
3486        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3487        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3488        #
3489        # if len(lots) != len(prices):
3490        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3491        #     raise Exception("Incorrect value")
3492        #
3493        # uLogger.debug("Extracted parameters for orders:")
3494        # uLogger.debug("lots = {}".format(lots))
3495        # uLogger.debug("prices = {}".format(prices))
3496        #
3497        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3498        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3499        # uLogger.debug("Order parameters: {}".format(result))
3500        #
3501        # return result
3502
3503    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3504        """
3505        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3506
3507        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3508        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3509        """
3510        result = False
3511        msg = "Instrument not defined!"
3512
3513        if portfolio is None or not portfolio:
3514            portfolio = self.Overview(show=False)
3515
3516        if self.ticker:
3517            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self.ticker))
3518            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3519
3520            for iType in TKS_INSTRUMENTS:
3521                for instrument in portfolio["stat"][iType]:
3522                    if instrument["ticker"] == self.ticker:
3523                        result = True
3524                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3525                        break
3526
3527        elif self.figi:
3528            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self.figi))
3529            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["figi"] == self.figi:
3534                        result = True
3535                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3536                        break
3537
3538        else:
3539            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3540
3541        uLogger.debug(msg)
3542
3543        return result
3544
3545    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3546        """
3547        Returns instrument from the user's portfolio if it presents there.
3548        Instrument must be defined by `ticker` (highly priority) or `figi`.
3549
3550        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3551        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3552        """
3553        result = None
3554        msg = "Instrument not defined!"
3555
3556        if portfolio is None or not portfolio:
3557            portfolio = self.Overview(show=False)
3558
3559        if self.ticker:
3560            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3561            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3562
3563            for iType in TKS_INSTRUMENTS:
3564                for instrument in portfolio["stat"][iType]:
3565                    if instrument["ticker"] == self.ticker:
3566                        result = instrument
3567                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3568                        break
3569
3570        elif self.figi:
3571            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3572            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3573
3574            for iType in TKS_INSTRUMENTS:
3575                for instrument in portfolio["stat"][iType]:
3576                    if instrument["figi"] == self.figi:
3577                        result = instrument
3578                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3579                        break
3580
3581        else:
3582            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3583
3584        uLogger.debug(msg)
3585
3586        return result
3587
3588    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3589        """
3590        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3591
3592        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3593
3594        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3595        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3596        """
3597        result = False
3598        msg = "Instrument not defined!"
3599
3600        if portfolio is None or not portfolio:
3601            portfolio = self.Overview(show=False)
3602
3603        if self.ticker:
3604            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker))
3605            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker)
3606
3607            for instrument in portfolio["stat"]["orders"]:
3608                if instrument["ticker"] == self.ticker:
3609                    result = True
3610                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker)
3611                    break
3612
3613        elif self.figi:
3614            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi))
3615            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi)
3616
3617            for instrument in portfolio["stat"]["orders"]:
3618                if instrument["figi"] == self.figi:
3619                    result = True
3620                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi)
3621                    break
3622
3623        else:
3624            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3625
3626        uLogger.debug(msg)
3627
3628        return result
3629
3630    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3631        """
3632        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3633        Instrument must be defined by `ticker` (highly priority) or `figi`.
3634
3635        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3636
3637        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3638        :return: list with `orderID`s of limit orders.
3639        """
3640        result = []
3641        msg = "Instrument not defined!"
3642
3643        if portfolio is None or not portfolio:
3644            portfolio = self.Overview(show=False)
3645
3646        if self.ticker:
3647            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker))
3648            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker)
3649
3650            for instrument in portfolio["stat"]["orders"]:
3651                if instrument["ticker"] == self.ticker:
3652                    result.append(instrument["orderID"])
3653
3654            if result:
3655                msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker)
3656
3657        elif self.figi:
3658            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi))
3659            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi)
3660
3661            for instrument in portfolio["stat"]["orders"]:
3662                if instrument["figi"] == self.figi:
3663                    result.append(instrument["orderID"])
3664
3665            if result:
3666                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi)
3667
3668        else:
3669            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3670
3671        uLogger.debug(msg)
3672
3673        return result
3674
3675    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3676        """
3677        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3678
3679        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3680
3681        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3682        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3683        """
3684        result = False
3685        msg = "Instrument not defined!"
3686
3687        if portfolio is None or not portfolio:
3688            portfolio = self.Overview(show=False)
3689
3690        if self.ticker:
3691            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker))
3692            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker)
3693
3694            for instrument in portfolio["stat"]["stopOrders"]:
3695                if instrument["ticker"] == self.ticker:
3696                    result = True
3697                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker)
3698                    break
3699
3700        elif self.figi:
3701            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi))
3702            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi)
3703
3704            for instrument in portfolio["stat"]["stopOrders"]:
3705                if instrument["figi"] == self.figi:
3706                    result = True
3707                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi)
3708                    break
3709
3710        else:
3711            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3712
3713        uLogger.debug(msg)
3714
3715        return result
3716
3717    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3718        """
3719        Returns list with all `orderID`s of opened stop orders for the instrument.
3720        Instrument must be defined by `ticker` (highly priority) or `figi`.
3721
3722        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3723
3724        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3725        :return: list with `orderID`s of stop orders.
3726        """
3727        result = []
3728        msg = "Instrument not defined!"
3729
3730        if portfolio is None or not portfolio:
3731            portfolio = self.Overview(show=False)
3732
3733        if self.ticker:
3734            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker))
3735            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker)
3736
3737            for instrument in portfolio["stat"]["stopOrders"]:
3738                if instrument["ticker"] == self.ticker:
3739                    result.append(instrument["orderID"])
3740
3741            if result:
3742                msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker)
3743
3744        elif self.figi:
3745            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi))
3746            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi)
3747
3748            for instrument in portfolio["stat"]["stopOrders"]:
3749                if instrument["figi"] == self.figi:
3750                    result.append(instrument["orderID"])
3751
3752            if result:
3753                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi)
3754
3755        else:
3756            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3757
3758        uLogger.debug(msg)
3759
3760        return result
3761
3762    def RequestLimits(self) -> dict:
3763        """
3764        Method for obtaining the available funds for withdrawal for current `accountId`.
3765
3766        See also:
3767        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3768        - `OverviewLimits()` method
3769
3770        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3771                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3772                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3773                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3774        """
3775        if self.accountId is None or not self.accountId:
3776            uLogger.error("Variable `accountId` must be defined for using this method!")
3777            raise Exception("Account ID required")
3778
3779        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3780
3781        self.body = str({"accountId": self.accountId})
3782        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3783        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3784
3785        if self.moreDebug:
3786            uLogger.debug("Records about available funds for withdrawal successfully received")
3787
3788        return rawLimits
3789
3790    def OverviewLimits(self, show: bool = False) -> dict:
3791        """
3792        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3793
3794        See also: `RequestLimits()`.
3795
3796        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3797        :return: dict with raw parsed data from server and some calculated statistics about it.
3798        """
3799        if self.accountId is None or not self.accountId:
3800            uLogger.error("Variable `accountId` must be defined for using this method!")
3801            raise Exception("Account ID required")
3802
3803        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3804
3805        view = {
3806            "rawLimits": rawLimits,
3807            "limits": {  # parsed data for every currency:
3808                "money": {  # this is an array of portfolio currency positions
3809                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3810                },
3811                "blocked": {  # this is an array of blocked currency
3812                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3813                },
3814                "blockedGuarantee": {  # this is locked money under collateral for futures
3815                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3816                },
3817            },
3818        }
3819
3820        # --- Prepare text table with limits in human-readable format:
3821        if show:
3822            info = [
3823                "# Withdrawal limits\n\n",
3824                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3825                "* **Account ID:** [{}]\n".format(self.accountId),
3826            ]
3827
3828            if view["limits"]["money"]:
3829                info.extend([
3830                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3831                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3832                ])
3833
3834            else:
3835                info.append("\nNo withdrawal limits\n")
3836
3837            for curr in view["limits"]["money"].keys():
3838                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3839                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3840                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3841
3842                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3843                    "[{}]".format(curr),
3844                    "{:.2f}".format(view["limits"]["money"][curr]),
3845                    "{:.2f}".format(availableMoney),
3846                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3847                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3848                )
3849
3850                if curr == "rub":
3851                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3852
3853                else:
3854                    info.append(infoStr)
3855
3856            infoText = "".join(info)
3857
3858            uLogger.info(infoText)
3859
3860            if self.withdrawalLimitsFile:
3861                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3862                    fH.write(infoText)
3863
3864                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3865
3866        return view
3867
3868    def RequestAccounts(self) -> dict:
3869        """
3870        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3871
3872        See also:
3873        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3874        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3875        - `OverviewUserInfo()` method
3876
3877        :return: dict with raw data from server that contains accounts info. Example of dict:
3878                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3879                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3880                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3881                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3882        """
3883        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3884
3885        self.body = str({})
3886        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3887        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3888
3889        if self.moreDebug:
3890            uLogger.debug("Records about available accounts successfully received")
3891
3892        return rawAccounts
3893
3894    def RequestUserInfo(self) -> dict:
3895        """
3896        Method for requesting common user's information.
3897
3898        See also:
3899        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3900        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3901        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3902        - `OverviewUserInfo()` method
3903
3904        :return: dict with raw data from server that contains user's information. Example of dict:
3905                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3906                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3907        """
3908        uLogger.debug("Requesting common user's information. Wait, please...")
3909
3910        self.body = str({})
3911        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3912        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3913
3914        if self.moreDebug:
3915            uLogger.debug("Records about current user successfully received")
3916
3917        return rawUserInfo
3918
3919    def RequestMarginStatus(self, accountId: str = None) -> dict:
3920        """
3921        Method for requesting margin calculation for defined account ID.
3922
3923        See also:
3924        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3925        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3926        - `OverviewUserInfo()` method
3927
3928        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3929        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3930                 Example of responses:
3931                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3932                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3933                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3934                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3935                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3936                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3937        """
3938        if accountId is None or not accountId:
3939            if self.accountId is None or not self.accountId:
3940                uLogger.error("Variable `accountId` must be defined for using this method!")
3941                raise Exception("Account ID required")
3942
3943            else:
3944                accountId = self.accountId  # use `self.accountId` (main ID) by default
3945
3946        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3947
3948        self.body = str({"accountId": accountId})
3949        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3950        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3951
3952        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3953            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3954            rawMargin = {}
3955
3956        else:
3957            if self.moreDebug:
3958                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3959
3960        return rawMargin
3961
3962    def RequestTariffLimits(self) -> dict:
3963        """
3964        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3965
3966        See also:
3967        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3968        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3969        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3970        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3971        - `OverviewUserInfo()` method
3972
3973        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3974                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3975                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3976        """
3977        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3978
3979        self.body = str({})
3980        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3981        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3982
3983        if self.moreDebug:
3984            uLogger.debug("Records with limits of current tariff successfully received")
3985
3986        return rawTariffLimits
3987
3988    def RequestBondCoupons(self, iJSON: dict) -> dict:
3989        """
3990        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3991        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3992        All dates are in UTC timezone.
3993
3994        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3995        Documentation:
3996        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3997        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3998
3999        See also: `ExtendBondsData()`.
4000
4001        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
4002                      If raw iJSON is not data of bond then server returns an error [400] with message:
4003                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4004        :return: dictionary with bond payment calendar. Response example
4005                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4006                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4007                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4008                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4009        """
4010        if iJSON["figi"] is None or not iJSON["figi"]:
4011            uLogger.error("FIGI must be defined for using this method!")
4012            raise Exception("FIGI required")
4013
4014        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4015        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4016
4017        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4018            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4019            self.figi,
4020            startDate,
4021            endDate,
4022        ))
4023
4024        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4025        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4026        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4027
4028        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4029            uLogger.warning("Instrument type is not bond!")
4030
4031        else:
4032            if self.moreDebug:
4033                uLogger.debug("Records about bond payment calendar successfully received")
4034
4035        return calendar
4036
4037    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4038        """
4039        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4040        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4041        coupon yields, current yields and some statistics etc.
4042
4043        WARNING! This is too long operation if a lot of bonds requested from broker server.
4044
4045        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4046
4047        :param instruments: list of strings with tickers or FIGIs.
4048        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4049                     for further used by data scientists or stock analytics.
4050        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4051                 In XLSX-file and Pandas DataFrame fields mean:
4052                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4053                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4054        """
4055        if instruments is None or not instruments:
4056            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4057            raise Exception("Ticker or FIGI required")
4058
4059        if isinstance(instruments, str):
4060            instruments = [instruments]
4061
4062        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4063
4064        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4065
4066        iCount = len(uniqueInstruments)
4067        tooLong = iCount >= 20
4068        if tooLong:
4069            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4070
4071        bonds = None
4072        for i, self.figi in enumerate(uniqueInstruments):
4073            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4074
4075            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4076                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4077                rawBond = self.SearchByFIGI(requestPrice=True)
4078
4079                # Widen raw data with UTC current time (iData["actualDateTime"]):
4080                actualDate = datetime.now(tzutc())
4081                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4082
4083                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4084                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4085
4086                # Replace some values with human-readable:
4087                iData["nominalCurrency"] = iData["nominal"]["currency"]
4088                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4089                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4090                iData["aciCurrency"] = iData["aciValue"]["currency"]
4091                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4092                iData["issueSize"] = int(iData["issueSize"])
4093                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4094                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4095                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4096                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4097                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4098                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4099                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4100                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4101                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4102                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4103
4104                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4105                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4106                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4107                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4108                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4109                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4110                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4111                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4112                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4113                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4114                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4115
4116                # Widen raw data with calendar data from `rawCalendar` values:
4117                calendarData = []
4118                if "events" in iData["rawCalendar"].keys():
4119                    for item in iData["rawCalendar"]["events"]:
4120                        calendarData.append({
4121                            "couponDate": item["couponDate"],
4122                            "couponNumber": int(item["couponNumber"]),
4123                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4124                            "payCurrency": item["payOneBond"]["currency"],
4125                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4126                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4127                            "couponStartDate": item["couponStartDate"],
4128                            "couponEndDate": item["couponEndDate"],
4129                            "couponPeriod": item["couponPeriod"],
4130                        })
4131
4132                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4133                    if "maturityDate" not in iData.keys():
4134                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4135
4136                # Widen raw data with Coupon Rate.
4137                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4138                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4139                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4140                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4141
4142                # Widen raw data with Yield to Maturity (YTM) on current date.
4143                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4144                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4145                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4146                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4147                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4148                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4149
4150                iData["calendar"] = calendarData  # adds calendar at the end
4151
4152                # Remove not used data:
4153                iData.pop("uid")
4154                iData.pop("positionUid")
4155                iData.pop("currentPrice")
4156                iData.pop("rawCalendar")
4157
4158                colNames = list(iData.keys())
4159                if bonds is None:
4160                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4161
4162                else:
4163                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4164
4165            else:
4166                uLogger.warning("Instrument is not a bond!")
4167
4168            processed = round(100 * (i + 1) / iCount, 1)
4169            if tooLong and processed % 5 == 0:
4170                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4171
4172            else:
4173                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4174
4175        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4176
4177        # Saving bonds from Pandas DataFrame to XLSX sheet:
4178        if xlsx and self.bondsXLSXFile:
4179            with pd.ExcelWriter(
4180                    path=self.bondsXLSXFile,
4181                    date_format=TKS_DATE_FORMAT,
4182                    datetime_format=TKS_DATE_TIME_FORMAT,
4183                    mode="w",
4184            ) as writer:
4185                bonds.to_excel(
4186                    writer,
4187                    sheet_name="Extended bonds data",
4188                    index=True,
4189                    encoding="UTF-8",
4190                    freeze_panes=(1, 1),
4191                )  # saving as XLSX-file with freeze first row and column as headers
4192
4193            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4194
4195        return bonds
4196
4197    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4198        """
4199        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4200
4201        WARNING! This is too long operation if a lot of bonds requested from broker server.
4202
4203        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4204
4205        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4206                        extended information about bonds: main info, current prices, bond payment calendar,
4207                        coupon yields, current yields and some statistics etc.
4208                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4209        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4210                     for further used by data scientists or stock analytics.
4211        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4212        """
4213        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4214            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4215
4216        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4217
4218        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4219        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4220        calendar = None
4221        for bond in extBonds.iterrows():
4222            for item in bond[1]["calendar"]:
4223                cData = {
4224                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4225                    "couponDate": item["couponDate"],
4226                    "figi": bond[1]["figi"],
4227                    "ticker": bond[1]["ticker"],
4228                    "name": bond[1]["name"],
4229                    "couponNumber": item["couponNumber"],
4230                    "payOneBond": item["payOneBond"],
4231                    "payCurrency": item["payCurrency"],
4232                    "couponType": item["couponType"],
4233                    "couponPeriod": item["couponPeriod"],
4234                    "fixDate": item["fixDate"],
4235                    "couponStartDate": item["couponStartDate"],
4236                    "couponEndDate": item["couponEndDate"],
4237                }
4238
4239                if calendar is None:
4240                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4241
4242                else:
4243                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4244
4245        if calendar is not None:
4246            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4247
4248            # Saving calendar from Pandas DataFrame to XLSX sheet:
4249            if xlsx:
4250                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4251
4252                with pd.ExcelWriter(
4253                        path=xlsxCalendarFile,
4254                        date_format=TKS_DATE_FORMAT,
4255                        datetime_format=TKS_DATE_TIME_FORMAT,
4256                        mode="w",
4257                ) as writer:
4258                    humanReadable = calendar.copy(deep=True)
4259                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4260                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4261                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4262                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4263                    humanReadable.columns = colNames  # human-readable column names
4264
4265                    humanReadable.to_excel(
4266                        writer,
4267                        sheet_name="Bond payments calendar",
4268                        index=False,
4269                        encoding="UTF-8",
4270                        freeze_panes=(1, 2),
4271                    )  # saving as XLSX-file with freeze first row and column as headers
4272
4273                    del humanReadable  # release df in memory
4274
4275                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4276
4277        return calendar
4278
4279    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4280        """
4281        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4282        Also, creates Markdown file with calendar data, `calendar.md` by default.
4283
4284        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4285
4286        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4287                        extended information about bonds: main info, current prices, bond payment calendar,
4288                        coupon yields, current yields and some statistics etc.
4289                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4290        :param show: if `True` then also printing bonds payment calendar to the console,
4291                     otherwise save to file `calendarFile` only. `False` by default.
4292        :return: multilines text in Markdown format with bonds payment calendar as a table.
4293        """
4294        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4295            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4296
4297        infoText = "# Bond payments calendar\n\n"
4298
4299        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4300
4301        if not (calendar is None or calendar.empty):
4302            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4303
4304            info = [
4305                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4306                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4307            ]
4308
4309            newMonth = False
4310            notOneBond = calendar["figi"].nunique() > 1
4311            for i, bond in enumerate(calendar.iterrows()):
4312                if newMonth and notOneBond:
4313                    info.append(splitLine)
4314
4315                info.append(
4316                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4317                        "  √" if bond[1]["paid"] else "  —",
4318                        bond[1]["couponDate"].split("T")[0],
4319                        bond[1]["figi"],
4320                        bond[1]["ticker"],
4321                        bond[1]["couponNumber"],
4322                        "{} {}".format(
4323                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4324                            bond[1]["payCurrency"],
4325                        ),
4326                        bond[1]["couponType"],
4327                        bond[1]["couponPeriod"],
4328                        bond[1]["fixDate"].split("T")[0],
4329                    )
4330                )
4331
4332                if i < len(calendar.values) - 1:
4333                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4334                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4335                    newMonth = False if curDate.month == nextDate.month else True
4336
4337                else:
4338                    newMonth = False
4339
4340            infoText += "".join(info)
4341
4342            if show:
4343                uLogger.info("{}".format(infoText))
4344
4345            if self.calendarFile is not None:
4346                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4347                    fH.write(infoText)
4348
4349                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4350
4351        else:
4352            infoText += "No data\n"
4353
4354        return infoText
4355
4356    def OverviewAccounts(self, show: bool = False) -> dict:
4357        """
4358        Method for parsing and show simple table with all available user accounts.
4359
4360        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4361
4362        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4363        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4364                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4365                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4366                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4367                                                        "closed": "—", "access": "Full access" }, ...}}`
4368        """
4369        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4370
4371        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4372        accounts = {
4373            item["id"]: {
4374                "type": TKS_ACCOUNT_TYPES[item["type"]],
4375                "name": item["name"],
4376                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4377                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4378                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4379                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4380            } for item in rawAccounts["accounts"]
4381        }
4382
4383        # Raw and parsed data with some fields replaced in "stat" section:
4384        view = {
4385            "rawAccounts": rawAccounts,
4386            "stat": accounts,
4387        }
4388
4389        # --- Prepare simple text table with only accounts data in human-readable format:
4390        if show:
4391            info = [
4392                "# User accounts\n\n",
4393                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4394                "| Account ID   | Type                      | Status                    | Name                           |\n",
4395                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4396            ]
4397
4398            for account in view["stat"].keys():
4399                info.extend([
4400                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4401                        account,
4402                        view["stat"][account]["type"],
4403                        view["stat"][account]["status"],
4404                        view["stat"][account]["name"],
4405                    )
4406                ])
4407
4408            infoText = "".join(info)
4409
4410            uLogger.info(infoText)
4411
4412            if self.userAccountsFile:
4413                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4414                    fH.write(infoText)
4415
4416                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4417
4418        return view
4419
4420    def OverviewUserInfo(self, show: bool = False) -> dict:
4421        """
4422        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4423
4424        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4425
4426        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4427        :return: dict with raw parsed data from server and some calculated statistics about it.
4428        """
4429        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4430        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4431        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4432        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4433        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4434        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4435
4436        # This is dict with parsed common user data:
4437        userInfo = {
4438            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4439            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4440            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4441            "tariff": rawUserInfo["tariff"],
4442        }
4443
4444        # This is an array of dict with parsed margin statuses for every account IDs:
4445        margins = {}
4446        for accountId in accounts.keys():
4447            if rawMargins[accountId]:
4448                margins[accountId] = {
4449                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4450                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4451                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4452                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4453                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4454                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4455                }
4456
4457            else:
4458                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4459
4460        unary = {}  # unary-connection limits
4461        for item in rawTariffLimits["unaryLimits"]:
4462            if item["limitPerMinute"] in unary.keys():
4463                unary[item["limitPerMinute"]].extend(item["methods"])
4464
4465            else:
4466                unary[item["limitPerMinute"]] = item["methods"]
4467
4468        stream = {}  # stream-connection limits
4469        for item in rawTariffLimits["streamLimits"]:
4470            if item["limit"] in stream.keys():
4471                stream[item["limit"]].extend(item["streams"])
4472
4473            else:
4474                stream[item["limit"]] = item["streams"]
4475
4476        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4477        limits = {
4478            "unary": unary,
4479            "stream": stream,
4480        }
4481
4482        # Raw and parsed data as an output result:
4483        view = {
4484            "rawUserInfo": rawUserInfo,
4485            "rawAccounts": rawAccounts,
4486            "rawMargins": rawMargins,
4487            "rawTariffLimits": rawTariffLimits,
4488            "stat": {
4489                "userInfo": userInfo,
4490                "accounts": accounts,
4491                "margins": margins,
4492                "limits": limits,
4493            },
4494        }
4495
4496        # --- Prepare text table with user information in human-readable format:
4497        if show:
4498            info = [
4499                "# Full user information\n\n",
4500                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4501                "## Common information\n\n",
4502                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4503                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4504                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4505                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4506                "\n## User accounts\n\n",
4507            ]
4508
4509            for account in view["stat"]["accounts"].keys():
4510                info.extend([
4511                    "### ID: [{}]\n\n".format(account),
4512                    "| Parameters           | Values                                                       |\n",
4513                    "|----------------------|--------------------------------------------------------------|\n",
4514                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4515                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4516                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4517                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4518                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4519                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4520                ])
4521
4522                if margins[account]:
4523                    info.extend([
4524                        "| Margin status:       | Enabled                                                      |\n",
4525                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4526                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4527                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4528                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4529                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4530                    ])
4531
4532                else:
4533                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4534
4535            info.extend([
4536                "\n## Current user tariff limits\n",
4537                "\nSee also:\n",
4538                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4539                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4540                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4541                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4542                "\n### Unary limits\n",
4543            ])
4544
4545            if unary:
4546                for key, values in sorted(unary.items()):
4547                    info.append("\n* Max requests per minute: {}\n".format(key))
4548
4549                    for value in values:
4550                        info.append("  - {}\n".format(value))
4551
4552            else:
4553                info.append("\nNot available\n")
4554
4555            info.append("\n### Stream limits\n")
4556
4557            if stream:
4558                for key, values in sorted(stream.items()):
4559                    info.append("\n* Max stream connections: {}\n".format(key))
4560
4561                    for value in values:
4562                        info.append("  - {}\n".format(value))
4563
4564            else:
4565                info.append("\nNot available\n")
4566
4567            infoText = "".join(info)
4568
4569            uLogger.info(infoText)
4570
4571            if self.userInfoFile:
4572                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4573                    fH.write(infoText)
4574
4575                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4576
4577        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 85        """
 86        Main class init.
 87
 88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 91        :param useCache: use default cache file with raw data to use instead of `iList`.
 92                         True by default. Cache is auto-update if new day has come.
 93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 94        :param defaultCache: path to default cache file. `dump.json` by default.
 95        """
 96        if token is None or not token:
 97            try:
 98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
100
101            except KeyError:
102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
103                raise Exception("Token required")
104
105        else:
106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
108
109        if accountId is None or not accountId:
110            try:
111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
113
114            except KeyError:
115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
116
117        else:
118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
120
121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
123
124        Latest version: https://pypi.org/project/tksbrokerapi/
125        """
126
127        self.aliases = TKS_TICKER_ALIASES
128        """Some aliases instead official tickers.
129
130        See also: `TKSEnums.TKS_TICKER_ALIASES`
131        """
132
133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
134
135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
136
137        self.ticker = ""
138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
139
140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
142
143        See also: `SearchByTicker()`, `SearchInstruments()`.
144        """
145
146        self.figi = ""
147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
148
149        See also: `SearchByFIGI()`, `SearchInstruments()`.
150        """
151
152        self.depth = 1
153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
154
155        See also: `GetCurrentPrices()`.
156        """
157
158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
160
161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
162        """
163
164        uLogger.debug("Broker API server: {}".format(self.server))
165
166        self.timeout = 15
167        """Server operations timeout in seconds. Default: `15`.
168
169        See also: `SendAPIRequest()`.
170        """
171
172        self.headers = {
173            "Content-Type": "application/json",
174            "accept": "application/json",
175            "Authorization": "Bearer {}".format(self.token),
176            "x-app-name": "Tim55667757.TKSBrokerAPI",
177        }
178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
179
180        See also: `SendAPIRequest()`.
181        """
182
183        self.body = None
184        """Request body which send to broker server. Default: `None`.
185
186        See also: `SendAPIRequest()`.
187        """
188
189        self.moreDebug = False
190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
191
192        self.historyFile = None
193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
194
195        See also: `History()`.
196        """
197
198        self.htmlHistoryFile = "index.html"
199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
200
201        See also: `ShowHistoryChart()`.
202        """
203
204        self.instrumentsFile = "instruments.md"
205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
206
207        See also: `ShowInstrumentsInfo()`.
208        """
209
210        self.searchResultsFile = "search-results.md"
211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
212
213        See also: `SearchInstruments()`.
214        """
215
216        self.pricesFile = "prices.md"
217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
218
219        See also: `GetListOfPrices()`.
220        """
221
222        self.infoFile = "info.md"
223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
224
225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
226        """
227
228        self.bondsXLSXFile = "ext-bonds.xlsx"
229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
231
232        See also: `ExtendBondsData()`.
233        """
234
235        self.calendarFile = "calendar.md"
236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
237        
238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
239
240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
241        """
242
243        self.overviewFile = "overview.md"
244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
245
246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
247        """
248
249        self.overviewDigestFile = "overview-digest.md"
250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
251
252        See also: `Overview()` with parameter `details="digest"`.
253        """
254
255        self.overviewPositionsFile = "overview-positions.md"
256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
257
258        See also: `Overview()` with parameter `details="positions"`.
259        """
260
261        self.overviewOrdersFile = "overview-orders.md"
262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
263
264        See also: `Overview()` with parameter `details="orders"`.
265        """
266
267        self.overviewAnalyticsFile = "overview-analytics.md"
268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
269
270        See also: `Overview()` with parameter `details="analytics"`.
271        """
272
273        self.overviewBondsCalendarFile = "overview-calendar.md"
274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
275
276        See also: `Overview()` with parameter `details="calendar"`.
277        """
278
279        self.reportFile = "deals.md"
280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
281
282        See also: `Deals()`.
283        """
284
285        self.withdrawalLimitsFile = "limits.md"
286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
287
288        See also: `OverviewLimits()` and `RequestLimits()`.
289        """
290
291        self.userInfoFile = "user-info.md"
292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
293
294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
295        """
296
297        self.userAccountsFile = "accounts.md"
298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
299
300        See also: `OverviewAccounts()`, `RequestAccounts()`.
301        """
302
303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
305
306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
307
308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
309        """
310
311        self.iList = None  # init iList for raw instruments data
312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
313        
314        See also: `Listing()`, `DumpInstruments()`.
315        """
316
317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
318        if useCache:
319            if os.path.exists(self.iListDumpFile):
320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
321                curTime = datetime.now(tzutc())
322
323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
325
326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
327
328                else:
329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
330
331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
332                        os.path.abspath(self.iListDumpFile),
333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
334                    ))
335
336            else:
337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
339
340        else:
341            self.iList = self.Listing()  # request new raw instruments data from broker server
342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
343
344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
346
347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
348        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
364    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
365        """
366        Send GET or POST request to broker server and receive JSON object.
367
368        self.header: must be defining with dictionary of headers.
369        self.body: if define then used as request body. None by default.
370        self.timeout: global request timeout, 15 seconds by default.
371        :param url: url with REST request.
372        :param reqType: send "GET" or "POST" request. "GET" by default.
373        :param retry: how many times retry after first request if an 5xx server errors occurred.
374        :param pause: sleep time in seconds between retries.
375        :return: response JSON (dictionary) from broker.
376        """
377        if reqType.upper() not in ("GET", "POST"):
378            uLogger.error("You can define request type: `GET` or `POST`!")
379            raise Exception("Incorrect value")
380
381        if self.moreDebug:
382            uLogger.debug("Request parameters:")
383            uLogger.debug("    - REST API URL: {}".format(url))
384            uLogger.debug("    - request type: {}".format(reqType))
385            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
386            uLogger.debug("    - body:\n{}".format(self.body))
387
388        # fast hack to avoid all operations with some tickers/FIGI
389        responseJSON = {}
390        oK = True
391        for item in self.exclude:
392            if item in url:
393                if self.moreDebug:
394                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
395
396                oK = False
397                break
398
399        if oK:
400            counter = 0
401            response = None
402            errMsg = ""
403
404            while not response and counter <= retry:
405                if reqType == "GET":
406                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
407
408                if reqType == "POST":
409                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
410
411                if self.moreDebug:
412                    uLogger.debug("Response:")
413                    uLogger.debug("    - status code: {}".format(response.status_code))
414                    uLogger.debug("    - reason: {}".format(response.reason))
415                    uLogger.debug("    - body length: {}".format(len(response.text)))
416                    uLogger.debug("    - headers:\n{}".format(response.headers))
417
418                # Server returns some headers:
419                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
420                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
421                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
422                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
423                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
424                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
425                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
426                    sleep(rateLimitWait)
427
428                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
429                if 400 <= response.status_code < 500:
430                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
431                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
432
433                    if "code" in response.text and "message" in response.text:
434                        msgDict = self._ParseJSON(rawData=response.text)
435                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
436
437                    counter = retry + 1  # do not retry for 4xx errors
438
439                if 500 <= response.status_code < 600:
440                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
441                    uLogger.debug("    - not oK, {}".format(errMsg))
442
443                    if "code" in response.text and "message" in response.text:
444                        errMsgDict = self._ParseJSON(rawData=response.text)
445                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
446
447                    counter += 1
448
449                    if counter <= retry:
450                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
451                        sleep(pause)
452
453            responseJSON = self._ParseJSON(rawData=response.text)
454
455            if errMsg:
456                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
457                uLogger.error("    - not oK, {}".format(errMsg))
458
459        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
492    def Listing(self) -> dict:
493        """
494        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
495
496        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
497        """
498        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
499        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
500
501        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
502        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
503        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
504
505        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
506        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
507        poolUpdater.close()
508
509        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
510        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
511        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
512
513        # calculate minimum price increment (step) for all instruments and set up instrument's type:
514        for iType in iList.keys():
515            for ticker in iList[iType]:
516                iList[iType][ticker]["type"] = iType
517
518                if "minPriceIncrement" in iList[iType][ticker].keys():
519                    iList[iType][ticker]["step"] = NanoToFloat(
520                        iList[iType][ticker]["minPriceIncrement"]["units"],
521                        iList[iType][ticker]["minPriceIncrement"]["nano"],
522                    )
523
524                else:
525                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
526
527        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
529    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
530        """
531        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
532
533        See also: `DumpInstruments()`, `Listing()`.
534
535        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
536                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
537        """
538        if self.iListDumpFile is None or not self.iListDumpFile:
539            uLogger.error("Output name of dump file must be defined!")
540            raise Exception("Filename required")
541
542        if not self.iList or forceUpdate:
543            self.iList = self.Listing()
544
545        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
546
547        # Save as XLSX with separated sheets for every type of instruments:
548        with pd.ExcelWriter(
549                path=xlsxDumpFile,
550                date_format=TKS_DATE_FORMAT,
551                datetime_format=TKS_DATE_TIME_FORMAT,
552                mode="w",
553        ) as writer:
554            for iType in TKS_INSTRUMENTS:
555                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
556                df = df[sorted(df)]  # sorted by column names
557                df = df.applymap(
558                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
559                    na_action="ignore",
560                )  # converting numbers from nano-type to float in every cell
561                df.to_excel(
562                    writer,
563                    sheet_name=iType,
564                    encoding="UTF-8",
565                    freeze_panes=(1, 1),
566                )  # saving as XLSX-file with freeze first row and column as headers
567
568        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
570    def DumpInstruments(self, forceUpdate: bool = True) -> str:
571        """
572        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
573        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
574
575        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
576
577        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
578                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
579        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
580        """
581        if self.iListDumpFile is None or not self.iListDumpFile:
582            uLogger.error("Output name of dump file must be defined!")
583            raise Exception("Filename required")
584
585        if not self.iList or forceUpdate:
586            self.iList = self.Listing()
587
588        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
589        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
590            fH.write(jsonDump)
591
592        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
593
594        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
596    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
597        """
598        Show information about one instrument defined by json data and prints it in Markdown format.
599
600        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
601
602        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
603        :param show: if `True` then also printing information about instrument and its current price.
604        :return: multilines text in Markdown format with information about one instrument.
605        """
606        splitLine = "|                                                             |                                                        |\n"
607        infoText = ""
608
609        if iJSON is not None and iJSON and isinstance(iJSON, dict):
610            info = [
611                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
612                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
613                "| Parameters                                                  | Values                                                 |\n",
614                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
615                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
616                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
617            ]
618
619            if "sector" in iJSON.keys() and iJSON["sector"]:
620                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
621
622            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
623                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
624
625            info.extend([
626                splitLine,
627                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
628                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
629            ])
630
631            if "isin" in iJSON.keys() and iJSON["isin"]:
632                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
633
634            if "classCode" in iJSON.keys():
635                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
636
637            info.extend([
638                splitLine,
639                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
640                splitLine,
641                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
642                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
643                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
644            ])
645
646            if iJSON["figi"]:
647                self.figi = iJSON["figi"]
648                iJSON = iJSON | self.RequestTradingStatus()
649
650                info.extend([
651                    splitLine,
652                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
653                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
654                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
655                ])
656
657            info.append(splitLine)
658
659            if "type" in iJSON.keys() and iJSON["type"]:
660                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
661
662                if "shareType" in iJSON.keys() and iJSON["shareType"]:
663                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
664
665            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
666                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
667
668            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
669                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
670
671            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
672                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
673
674            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
675                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
676
677            if "focusType" in iJSON.keys() and iJSON["focusType"]:
678                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
679
680            if "assetType" in iJSON.keys() and iJSON["assetType"]:
681                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
682
683            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
684                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
685
686            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
687                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
688
689            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
690                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
691
692            if "currency" in iJSON.keys():
693                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
694
695            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
696                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
697
698            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
699                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
700
701            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
702                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
703
704            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
705                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
706
707            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
708                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
709
710            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
711                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
712
713            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
714                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
715
716            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
717                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
718
719            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
720                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
721
722            iExt = None
723            if iJSON["type"] == "Bonds":
724                info.extend([
725                    splitLine,
726                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
727                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
728                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
729                        iJSON["nominal"]["currency"],
730                    )),
731                ])
732
733                if "floatingCouponFlag" in iJSON.keys():
734                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
735
736                if "amortizationFlag" in iJSON.keys():
737                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
738
739                info.append(splitLine)
740
741                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
742                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
743
744                if iJSON["figi"]:
745                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
746
747                    info.extend([
748                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
749                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
750                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
751                    ])
752
753                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
754                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
755                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
756                        iJSON["aciValue"]["currency"]
757                    )))
758
759            if "currentPrice" in iJSON.keys():
760                info.append(splitLine)
761
762                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
763                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
764
765                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
766                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
767                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
768                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
769                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
770
771                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
772                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
773
774                info.extend([
775                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
776                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
777                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
778                    )),
779                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
780                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
781                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
782                    )),
783                    "| Changes between last deal price and last close              | {:<54} |\n".format(
784                        "{:.2f}%{}".format(
785                            iJSON["currentPrice"]["changes"],
786                            " ({}{:.2f} {})".format(
787                                "+" if bondChangesDelta > 0 else "",
788                                bondChangesDelta,
789                                aciCurrency
790                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
791                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
792                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
793                                currency
794                            ),
795                        )
796                    ),
797                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
798                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
799                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
800                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
801                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
802                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
803                    )),
804                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
805                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
806                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
807                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
808                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
809                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
810                    )),
811                ])
812
813            if "lot" in iJSON.keys():
814                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
815
816            if "step" in iJSON.keys() and iJSON["step"] != 0:
817                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
818
819            # Add bond payment calendar:
820            if iJSON["type"] == "Bonds":
821                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
822                info.extend(["\n", strCalendar])
823
824            infoText += "".join(info)
825
826            if show:
827                uLogger.info("{}".format(infoText))
828
829            else:
830                uLogger.debug("{}".format(infoText))
831
832            if self.infoFile is not None:
833                with open(self.infoFile, "w", encoding="UTF-8") as fH:
834                    fH.write(infoText)
835
836                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
837
838        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
840    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
841        """
842        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
843
844        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
845        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
846        :return: JSON formatted data with information about instrument.
847        """
848        tickerJSON = {}
849        if self.moreDebug:
850            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
851
852        if not self.ticker:
853            uLogger.warning("self.ticker variable is not be empty!")
854
855        else:
856            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
857                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
858                raise Exception("Instrument not allowed")
859
860            if not self.iList:
861                self.iList = self.Listing()
862
863            if self.ticker in self.iList["Shares"].keys():
864                tickerJSON = self.iList["Shares"][self.ticker]
865                if self.moreDebug:
866                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
867
868            elif self.ticker in self.iList["Currencies"].keys():
869                tickerJSON = self.iList["Currencies"][self.ticker]
870                if self.moreDebug:
871                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
872
873            elif self.ticker in self.iList["Bonds"].keys():
874                tickerJSON = self.iList["Bonds"][self.ticker]
875                if self.moreDebug:
876                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
877
878            elif self.ticker in self.iList["Etfs"].keys():
879                tickerJSON = self.iList["Etfs"][self.ticker]
880                if self.moreDebug:
881                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
882
883            elif self.ticker in self.iList["Futures"].keys():
884                tickerJSON = self.iList["Futures"][self.ticker]
885                if self.moreDebug:
886                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
887
888        if tickerJSON:
889            self.figi = tickerJSON["figi"]
890
891            if requestPrice:
892                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
893
894                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
895                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
896
897                else:
898                    tickerJSON["currentPrice"]["changes"] = 0
899
900            if show:
901                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
902
903        else:
904            if show:
905                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
906
907        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 909    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 910        """
 911        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 912
 913        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 914        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 915        :return: JSON formatted data with information about instrument.
 916        """
 917        figiJSON = {}
 918        if self.moreDebug:
 919            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 920
 921        if not self.figi:
 922            uLogger.warning("self.figi variable is not be empty!")
 923
 924        else:
 925            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 926                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 927                raise Exception("Instrument not allowed")
 928
 929            if not self.iList:
 930                self.iList = self.Listing()
 931
 932            for item in self.iList["Shares"].keys():
 933                if self.figi == self.iList["Shares"][item]["figi"]:
 934                    figiJSON = self.iList["Shares"][item]
 935
 936                    if self.moreDebug:
 937                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 938
 939                    break
 940
 941            if not figiJSON:
 942                for item in self.iList["Currencies"].keys():
 943                    if self.figi == self.iList["Currencies"][item]["figi"]:
 944                        figiJSON = self.iList["Currencies"][item]
 945
 946                        if self.moreDebug:
 947                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 948
 949                        break
 950
 951            if not figiJSON:
 952                for item in self.iList["Bonds"].keys():
 953                    if self.figi == self.iList["Bonds"][item]["figi"]:
 954                        figiJSON = self.iList["Bonds"][item]
 955
 956                        if self.moreDebug:
 957                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 958
 959                        break
 960
 961            if not figiJSON:
 962                for item in self.iList["Etfs"].keys():
 963                    if self.figi == self.iList["Etfs"][item]["figi"]:
 964                        figiJSON = self.iList["Etfs"][item]
 965
 966                        if self.moreDebug:
 967                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 968
 969                        break
 970
 971            if not figiJSON:
 972                for item in self.iList["Futures"].keys():
 973                    if self.figi == self.iList["Futures"][item]["figi"]:
 974                        figiJSON = self.iList["Futures"][item]
 975
 976                        if self.moreDebug:
 977                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 978
 979                        break
 980
 981        if figiJSON:
 982            self.figi = figiJSON["figi"]
 983            self.ticker = figiJSON["ticker"]
 984
 985            if requestPrice:
 986                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 987
 988                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 989                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 990
 991                else:
 992                    figiJSON["currentPrice"]["changes"] = 0
 993
 994            if show:
 995                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 996
 997        else:
 998            if show:
 999                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1000
1001        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1003    def GetCurrentPrices(self, show: bool = True) -> dict:
1004        """
1005        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1006        `{"buy": [{"price": 1243.8, "quantity": 193},
1007                  {"price": 1244.0, "quantity": 168},
1008                  {"price": 1244.8, "quantity": 5},
1009                  {"price": 1245.0, "quantity": 61},
1010                  {"price": 1245.4, "quantity": 60}],
1011          "sell": [{"price": 1243.6, "quantity": 8},
1012                   {"price": 1242.6, "quantity": 10},
1013                   {"price": 1242.4, "quantity": 18},
1014                   {"price": 1242.2, "quantity": 50},
1015                   {"price": 1242.0, "quantity": 113}],
1016          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1017        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1018        - sell: list of dicts with Buyers prices,
1019            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1020            - quantity: volume value by current price in lots,
1021        - limitUp: current trade session limit price, maximum,
1022        - limitDown: current trade session limit price, minimum,
1023        - lastPrice: last deal price of the instrument,
1024        - closePrice: previous trade session close price of the instrument.
1025
1026        See also: `SearchByTicker()` and `SearchByFIGI()`.
1027        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1028        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1029
1030        :param show: if `True` then print DOM to log and console.
1031        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1032                 If an error occurred then returns an empty record:
1033                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1034        """
1035        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1036
1037        if self.depth < 1:
1038            uLogger.error("Depth of Market (DOM) must be >=1!")
1039            raise Exception("Incorrect value")
1040
1041        if not (self.ticker or self.figi):
1042            uLogger.error("self.ticker or self.figi variables must be defined!")
1043            raise Exception("Ticker or FIGI required")
1044
1045        if self.ticker and not self.figi:
1046            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1047            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1048
1049        if not self.ticker and self.figi:
1050            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1051            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1052
1053        if not self.figi:
1054            uLogger.error("FIGI is not defined!")
1055            raise Exception("Ticker or FIGI required")
1056
1057        else:
1058            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1059
1060            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1061            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1062            self.body = str({"figi": self.figi, "depth": self.depth})
1063            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1064
1065            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1066                # list of dicts with sellers orders:
1067                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1068
1069                # list of dicts with buyers orders:
1070                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1071
1072                # max price of instrument at this time:
1073                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1074
1075                # min price of instrument at this time:
1076                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1077
1078                # last price of deal with instrument:
1079                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1080
1081                # last close price of instrument:
1082                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1083
1084            else:
1085                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1086                uLogger.debug("Server response: {}".format(pricesResponse))
1087
1088            if show:
1089                if prices["buy"] or prices["sell"]:
1090                    info = [
1091                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1092                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1093                            self.ticker,
1094                            self.figi,
1095                            self.depth,
1096                        ),
1097                        "-" * 60, "\n",
1098                        "             Orders of Buyers | Orders of Sellers\n",
1099                        "-" * 60, "\n",
1100                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1101                        "-" * 60, "\n",
1102                    ]
1103
1104                    if not prices["buy"]:
1105                        info.append("                              | No orders!\n")
1106                        sumBuy = 0
1107
1108                    else:
1109                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1110                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1111                        for item in maxMinSorted:
1112                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1113
1114                    if not prices["sell"]:
1115                        info.append("No orders!                    |\n")
1116                        sumSell = 0
1117
1118                    else:
1119                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1120                        for item in prices["sell"]:
1121                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1122
1123                    info.extend([
1124                        "-" * 60, "\n",
1125                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1126                        "-" * 60, "\n",
1127                    ])
1128
1129                    infoText = "".join(info)
1130
1131                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1132
1133                else:
1134                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1135
1136        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1138    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1139        """
1140        This method get and show information about all available broker instruments for current user account.
1141        If `instrumentsFile` string is not empty then also save information to this file.
1142
1143        :param show: if `True` then print results to console, if `False` — print only to file.
1144        :return: multi-lines string with all available broker instruments
1145        """
1146        if not self.iList:
1147            self.iList = self.Listing()
1148
1149        info = [
1150            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1151            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1152        ]
1153
1154        # add instruments count by type:
1155        for iType in self.iList.keys():
1156            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1157
1158        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1159        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1160
1161        # generating info tables with all instruments by type:
1162        for iType in self.iList.keys():
1163            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1164
1165            for instrument in self.iList[iType].keys():
1166                iName = self.iList[iType][instrument]["name"]  # instrument's name
1167                if len(iName) > 57:
1168                    iName = "{}...".format(iName[:54])  # right trim for a long string
1169
1170                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1171                    self.iList[iType][instrument]["ticker"],
1172                    iName,
1173                    self.iList[iType][instrument]["figi"],
1174                    self.iList[iType][instrument]["currency"],
1175                    self.iList[iType][instrument]["lot"],
1176                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1177                ))
1178
1179        infoText = "".join(info)
1180
1181        if show:
1182            uLogger.info(infoText)
1183
1184        if self.instrumentsFile:
1185            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1186                fH.write(infoText)
1187
1188            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1189
1190        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1192    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1193        """
1194        This method search and show information about instruments by part of its ticker, FIGI or name.
1195        If `searchResultsFile` string is not empty then also save information to this file.
1196
1197        :param pattern: string with part of ticker, FIGI or instrument's name.
1198        :param show: if `True` then print results to console, if `False` — return list of result only.
1199        :return: list of dictionaries with all found instruments.
1200        """
1201        if not self.iList:
1202            self.iList = self.Listing()
1203
1204        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1205        compiledPattern = re.compile(pattern, re.IGNORECASE)
1206
1207        for iType in self.iList:
1208            for instrument in self.iList[iType].values():
1209                searchResult = compiledPattern.search(" ".join(
1210                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1211                ))
1212
1213                if searchResult:
1214                    searchResults[iType][instrument["ticker"]] = instrument
1215
1216        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1217        info = [
1218            "# Search results\n\n",
1219            "* **Search pattern:** [{}]\n".format(pattern),
1220            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1221            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1222        ]
1223        infoShort = info[:]
1224
1225        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1226        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1227        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1228
1229        if resultsLen == 0:
1230            info.append("\nNo results\n")
1231            infoShort.append("\nNo results\n")
1232            uLogger.warning("No results. Try changing your search pattern.")
1233
1234        else:
1235            for iType in searchResults:
1236                iTypeValuesCount = len(searchResults[iType].values())
1237                if iTypeValuesCount > 0:
1238                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1239                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1240
1241                    for instrument in searchResults[iType].values():
1242                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1243                            instrument["type"],
1244                            instrument["ticker"],
1245                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1246                            instrument["figi"],
1247                        ))
1248
1249                    if iTypeValuesCount <= 5:
1250                        infoShort.extend(info[-iTypeValuesCount:])
1251
1252                    else:
1253                        infoShort.extend(info[-5:])
1254                        infoShort.append(skippedLine)
1255
1256        infoText = "".join(info)
1257        infoTextShort = "".join(infoShort)
1258
1259        if show:
1260            uLogger.info(infoTextShort)
1261            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1262
1263        if self.searchResultsFile:
1264            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1265                fH.write(infoText)
1266
1267            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1268
1269        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1271    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1272        """
1273        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1274
1275        :param instruments: list of strings with tickers or FIGIs.
1276        :return: list with unique instrument FIGIs only.
1277        """
1278        requestedInstruments = []
1279        for iName in instruments:
1280            if iName not in self.aliases.keys():
1281                if iName not in requestedInstruments:
1282                    requestedInstruments.append(iName)
1283
1284            else:
1285                if iName not in requestedInstruments:
1286                    if self.aliases[iName] not in requestedInstruments:
1287                        requestedInstruments.append(self.aliases[iName])
1288
1289        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1290
1291        onlyUniqueFIGIs = []
1292        for iName in requestedInstruments:
1293            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1294                continue
1295
1296            self.ticker = iName
1297            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1298
1299            if not iData:
1300                self.ticker = ""
1301                self.figi = iName
1302
1303                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1304
1305                if not iData:
1306                    self.figi = ""
1307                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1308
1309            if iData and iData["figi"] not in onlyUniqueFIGIs:
1310                onlyUniqueFIGIs.append(iData["figi"])
1311
1312        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1313
1314        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1316    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1317        """
1318        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1319
1320        See limits: https://tinkoff.github.io/investAPI/limits/
1321
1322        If `pricesFile` string is not empty then also save information to this file.
1323
1324        :param instruments: list of strings with tickers or FIGIs.
1325        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1326        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1327                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1328        """
1329        if instruments is None or not instruments:
1330            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1331            raise Exception("Ticker or FIGI required")
1332
1333        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1334
1335        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1336
1337        iList = []  # trying to get info and current prices about all unique instruments:
1338        for self.figi in onlyUniqueFIGIs:
1339            iData = self.SearchByFIGI(requestPrice=True)
1340            iList.append(iData)
1341
1342        self.ShowListOfPrices(iList, show)
1343
1344        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1346    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1347        """
1348        Show table contains current prices of given instruments.
1349
1350        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1351                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1352        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1353        :return: multilines text in Markdown format as a table contains current prices.
1354        """
1355        infoText = ""
1356
1357        if show or self.pricesFile:
1358            info = [
1359                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1360                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1361                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1362            ]
1363
1364            for item in iList:
1365                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1366                    item["ticker"],
1367                    item["figi"],
1368                    item["type"],
1369                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1370                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1371                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1372                    "{} / {}".format(
1373                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1374                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1375                    ),
1376                    "{} / {}".format(
1377                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1378                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1379                    ),
1380                    item["currency"],
1381                ))
1382
1383            infoText = "".join(info)
1384
1385            if show:
1386                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1387
1388            if self.pricesFile:
1389                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1390                    fH.write(infoText)
1391
1392                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1393
1394        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1396    def RequestTradingStatus(self) -> dict:
1397        """
1398        Requesting trading status for the instrument defined by `figi` variable.
1399
1400        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1401
1402        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1403
1404        :return: dictionary with trading status attributes. Response example:
1405                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1406                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1407        """
1408        if self.figi is None or not self.figi:
1409            uLogger.error("Variable `figi` must be defined for using this method!")
1410            raise Exception("FIGI required")
1411
1412        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1413
1414        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1415        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1416        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1417
1418        if self.moreDebug:
1419            uLogger.debug("Records about current trading status successfully received")
1420
1421        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1423    def RequestPortfolio(self) -> dict:
1424        """
1425        Requesting actual user's portfolio for current `accountId`.
1426
1427        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1428
1429        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1430
1431        :return: dictionary with user's portfolio.
1432        """
1433        if self.accountId is None or not self.accountId:
1434            uLogger.error("Variable `accountId` must be defined for using this method!")
1435            raise Exception("Account ID required")
1436
1437        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1438
1439        self.body = str({"accountId": self.accountId})
1440        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1441        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1442
1443        if self.moreDebug:
1444            uLogger.debug("Records about user's portfolio successfully received")
1445
1446        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1448    def RequestPositions(self) -> dict:
1449        """
1450        Requesting open positions by currencies and instruments for current `accountId`.
1451
1452        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1453
1454        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1455
1456        :return: dictionary with open positions by instruments.
1457        """
1458        if self.accountId is None or not self.accountId:
1459            uLogger.error("Variable `accountId` must be defined for using this method!")
1460            raise Exception("Account ID required")
1461
1462        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1463
1464        self.body = str({"accountId": self.accountId})
1465        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1466        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1467
1468        if self.moreDebug:
1469            uLogger.debug("Records about current open positions successfully received")
1470
1471        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1473    def RequestPendingOrders(self) -> list:
1474        """
1475        Requesting current actual pending limit orders for current `accountId`.
1476
1477        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1478
1479        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1480
1481        :return: list of dictionaries with pending limit orders.
1482        """
1483        if self.accountId is None or not self.accountId:
1484            uLogger.error("Variable `accountId` must be defined for using this method!")
1485            raise Exception("Account ID required")
1486
1487        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1488
1489        self.body = str({"accountId": self.accountId})
1490        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1491        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1492
1493        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1494
1495        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1497    def RequestStopOrders(self) -> list:
1498        """
1499        Requesting current actual stop orders for current `accountId`.
1500
1501        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1502
1503        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1504
1505        :return: list of dictionaries with stop orders.
1506        """
1507        if self.accountId is None or not self.accountId:
1508            uLogger.error("Variable `accountId` must be defined for using this method!")
1509            raise Exception("Account ID required")
1510
1511        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1512
1513        self.body = str({"accountId": self.accountId})
1514        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1515        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1516
1517        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1518
1519        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1521    def Overview(self, show: bool = False, details: str = "full") -> dict:
1522        """
1523        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1524        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1525        and `overviewBondsCalendarFile` are defined then also save information to file.
1526
1527        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1528        many requests about the state of the portfolio, and then, based on the received data, a large number
1529        of calculation and statistics are collected.
1530
1531        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1532        :param details: how detailed should the information be?
1533        - `full` — shows full available information about portfolio status (by default),
1534        - `positions` — shows only open positions,
1535        - `orders` — shows only sections of open limits and stop orders.
1536        - `digest` — show a short digest of the portfolio status,
1537        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1538        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1539        :return: dictionary with client's raw portfolio and some statistics.
1540        """
1541        if self.accountId is None or not self.accountId:
1542            uLogger.error("Variable `accountId` must be defined for using this method!")
1543            raise Exception("Account ID required")
1544
1545        view = {
1546            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1547                "headers": {},  # list of dictionaries, response headers without "positions" section
1548                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1549                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1550                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1551                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1552                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1553                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1554                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1555                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1556                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1557            },
1558            "stat": {  # --- some statistics calculated using "raw" sections:
1559                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1560                "availableRUB": 0.,  # available rubles (without other currencies)
1561                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1562                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1563                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1564                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1565                "sharesCostRUB": 0.,  # costs of all shares in RUB
1566                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1567                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1568                "futuresCostRUB": 0.,  # costs of all futures in RUB
1569                "Currencies": [],  # list of dictionaries of all currencies statistics
1570                "Shares": [],  # list of dictionaries of all shares statistics
1571                "Bonds": [],  # list of dictionaries of all bonds statistics
1572                "Etfs": [],  # list of dictionaries of all etfs statistics
1573                "Futures": [],  # list of dictionaries of all futures statistics
1574                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1575                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1576                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1577                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1578                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1579            },
1580            "analytics": {  # --- some analytics of portfolio:
1581                "distrByAssets": {},  # portfolio distribution by assets
1582                "distrByCompanies": {},  # portfolio distribution by companies
1583                "distrBySectors": {},  # portfolio distribution by sectors
1584                "distrByCurrencies": {},  # portfolio distribution by currencies
1585                "distrByCountries": {},  # portfolio distribution by countries
1586                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1587            }
1588        }
1589
1590        details = details.lower()
1591        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1592        if details not in availableDetails:
1593            details = "full"
1594            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1595
1596        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1597
1598        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1599        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1600        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1601        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1602
1603        # save response headers without "positions" section:
1604        for key in portfolioResponse.keys():
1605            if key != "positions":
1606                view["raw"]["headers"][key] = portfolioResponse[key]
1607
1608            else:
1609                continue
1610
1611        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1612        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1613        for item in portfolioResponse["positions"]:
1614            if item["instrumentType"] == "currency":
1615                self.figi = item["figi"]
1616                curr = self.SearchByFIGI(requestPrice=False)
1617
1618                # current price of currency in RUB:
1619                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1620                    "name": curr["name"],
1621                    "currentPrice": NanoToFloat(
1622                        item["currentPrice"]["units"],
1623                        item["currentPrice"]["nano"]
1624                    ),
1625                }
1626
1627                view["raw"]["Currencies"].append(item)
1628
1629            elif item["instrumentType"] == "share":
1630                view["raw"]["Shares"].append(item)
1631
1632            elif item["instrumentType"] == "bond":
1633                view["raw"]["Bonds"].append(item)
1634
1635            elif item["instrumentType"] == "etf":
1636                view["raw"]["Etfs"].append(item)
1637
1638            elif item["instrumentType"] == "futures":
1639                view["raw"]["Futures"].append(item)
1640
1641            else:
1642                continue
1643
1644        # how many volume of currencies (by ISO currency name) are blocked:
1645        for item in view["raw"]["positions"]["blocked"]:
1646            blocked = NanoToFloat(item["units"], item["nano"])
1647            if blocked > 0:
1648                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1649
1650        # how many volume of instruments (by FIGI) are blocked:
1651        for item in view["raw"]["positions"]["securities"]:
1652            blocked = int(item["blocked"])
1653            if blocked > 0:
1654                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1655
1656        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1657
1658        if "rub" in allBlocked.keys():
1659            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1660
1661        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1662        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1663        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1664        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1665        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1666        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1667        view["stat"]["portfolioCostRUB"] = sum([
1668            view["stat"]["allCurrenciesCostRUB"],
1669            view["stat"]["sharesCostRUB"],
1670            view["stat"]["bondsCostRUB"],
1671            view["stat"]["etfsCostRUB"],
1672            view["stat"]["futuresCostRUB"],
1673        ])
1674
1675        # --- calculating some portfolio statistics:
1676        byComp = {}  # distribution by companies
1677        bySect = {}  # distribution by sectors
1678        byCurr = {}  # distribution by currencies (include RUB)
1679        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1680        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1681
1682        for item in portfolioResponse["positions"]:
1683            self.figi = item["figi"]
1684            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1685
1686            if instrument:
1687                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1688                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1689
1690                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1691                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1692
1693                else:
1694                    blocked = 0
1695
1696                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1697                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1698                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1699                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1700                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1701                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1702                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1703                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1704                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1705                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1706                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1707                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1708
1709                statData = {
1710                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1711                    "ticker": instrument["ticker"],  # ticker by FIGI
1712                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1713                    "volume": volume,  # available volume of instrument
1714                    "lots": lots,  # volume in lots of instrument
1715                    "direction": direction,  # direction of an instrument's position: short or long
1716                    "blocked": blocked,  # blocked volume of currency or instrument
1717                    "currentPrice": curPrice,  # current instrument's price in basic asset
1718                    "average": average,  # current average position price
1719                    "cost": cost,  # current cost of all volume of instrument in basic asset
1720                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1721                    "costRUB": costRUB,  # cost of instrument in ruble
1722                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1723                    "profit": profit,  # expected profit at current moment
1724                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1725                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1726                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1727                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1728                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1729                    "step": instrument["step"],  # minimum price increment
1730                }
1731
1732                # adding distribution by unique countries:
1733                if statData["country"] not in byCountry.keys():
1734                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1735
1736                else:
1737                    byCountry[statData["country"]]["cost"] += costRUB
1738                    byCountry[statData["country"]]["percent"] += percentCostRUB
1739
1740                if item["instrumentType"] != "currency":
1741                    # adding distribution by unique companies:
1742                    if statData["name"]:
1743                        if statData["name"] not in byComp.keys():
1744                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1745
1746                        else:
1747                            byComp[statData["name"]]["cost"] += costRUB
1748                            byComp[statData["name"]]["percent"] += percentCostRUB
1749
1750                    # adding distribution by unique sectors:
1751                    if statData["sector"] not in bySect.keys():
1752                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1753
1754                    else:
1755                        bySect[statData["sector"]]["cost"] += costRUB
1756                        bySect[statData["sector"]]["percent"] += percentCostRUB
1757
1758                # adding distribution by unique currencies:
1759                if currency not in byCurr.keys():
1760                    byCurr[currency] = {
1761                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1762                        "cost": costRUB,
1763                        "percent": percentCostRUB
1764                    }
1765
1766                else:
1767                    byCurr[currency]["cost"] += costRUB
1768                    byCurr[currency]["percent"] += percentCostRUB
1769
1770                # saving statistics for every instrument:
1771                if item["instrumentType"] == "currency":
1772                    view["stat"]["Currencies"].append(statData)
1773
1774                    # update dict with free funds for trading (total - blocked) by currencies
1775                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1776                    view["stat"]["funds"][currency] = {
1777                        "total": volume,
1778                        "totalCostRUB": costRUB,  # total volume cost in rubles
1779                        "free": volume - blocked,
1780                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1781                    }
1782
1783                elif item["instrumentType"] == "share":
1784                    view["stat"]["Shares"].append(statData)
1785
1786                elif item["instrumentType"] == "bond":
1787                    view["stat"]["Bonds"].append(statData)
1788
1789                elif item["instrumentType"] == "etf":
1790                    view["stat"]["Etfs"].append(statData)
1791
1792                elif item["instrumentType"] == "Futures":
1793                    view["stat"]["Futures"].append(statData)
1794
1795                else:
1796                    continue
1797
1798        # total changes in Russian Ruble:
1799        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1800        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1801        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1802        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1803        view["stat"]["funds"]["rub"] = {
1804            "total": view["stat"]["availableRUB"],
1805            "totalCostRUB": view["stat"]["availableRUB"],
1806            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1807            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1808        }
1809
1810        # --- pending limit orders sector data:
1811        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1812        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1813
1814        for item in view["raw"]["orders"]:
1815            self.figi = item["figi"]
1816
1817            if item["figi"] not in uniquePendingOrdersFIGIs:
1818                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1819
1820                uniquePendingOrdersFIGIs.append(item["figi"])
1821                uniquePendingOrders[item["figi"]] = instrument
1822
1823            else:
1824                instrument = uniquePendingOrders[item["figi"]]
1825
1826            if instrument:
1827                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1828                orderType = TKS_ORDER_TYPES[item["orderType"]]
1829                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1830                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1831
1832                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1833                if item["direction"] == "ORDER_DIRECTION_BUY":
1834                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1835
1836                else:
1837                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1838
1839                # requested price for order execution:
1840                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1841
1842                # necessary changes in percent to reach target from current price:
1843                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1844
1845                view["stat"]["orders"].append({
1846                    "orderID": item["orderId"],  # orderId number parameter of current order
1847                    "figi": item["figi"],  # FIGI identification
1848                    "ticker": instrument["ticker"],  # ticker name by FIGI
1849                    "lotsRequested": item["lotsRequested"],  # requested lots value
1850                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1851                    "currentPrice": lastPrice,  # current instrument's price for defined action
1852                    "targetPrice": target,  # requested price for order execution in base currency
1853                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1854                    "percentChanges": changes,  # changes in percent to target from current price
1855                    "currency": item["currency"],  # instrument's currency name
1856                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1857                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1858                    "status": orderState,  # order status from TKS_ORDER_STATES
1859                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1860                })
1861
1862        # --- stop orders sector data:
1863        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1864        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1865
1866        for item in view["raw"]["stopOrders"]:
1867            self.figi = item["figi"]
1868
1869            if item["figi"] not in uniqueStopOrdersFIGIs:
1870                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1871
1872                uniqueStopOrdersFIGIs.append(item["figi"])
1873                uniqueStopOrders[item["figi"]] = instrument
1874
1875            else:
1876                instrument = uniqueStopOrders[item["figi"]]
1877
1878            if instrument:
1879                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1880                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1881                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1882
1883                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1884                if "expirationTime" in item.keys():
1885                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1886                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1887
1888                else:
1889                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1890                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1891
1892                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1893                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1894                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1895
1896                else:
1897                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1898
1899                # requested price when stop-order executed:
1900                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1901
1902                # price for limit-order, set up when stop-order executed:
1903                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1904
1905                # necessary changes in percent to reach target from current price:
1906                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1907
1908                view["stat"]["stopOrders"].append({
1909                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1910                    "figi": item["figi"],  # FIGI identification
1911                    "ticker": instrument["ticker"],  # ticker name by FIGI
1912                    "lotsRequested": item["lotsRequested"],  # requested lots value
1913                    "currentPrice": lastPrice,  # current instrument's price for defined action
1914                    "targetPrice": target,  # requested price for stop-order execution in base currency
1915                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1916                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1917                    "percentChanges": changes,  # changes in percent to target from current price
1918                    "currency": item["currency"],  # instrument's currency name
1919                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1920                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1921                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1922                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1923                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1924                })
1925
1926        # --- calculating data for analytics section:
1927        # portfolio distribution by assets:
1928        view["analytics"]["distrByAssets"] = {
1929            "Ruble": {
1930                "uniques": 1,
1931                "cost": view["stat"]["availableRUB"],
1932                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1933            },
1934            "Currencies": {
1935                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1936                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1937                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1938            },
1939            "Shares": {
1940                "uniques": len(view["stat"]["Shares"]),
1941                "cost": view["stat"]["sharesCostRUB"],
1942                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1943            },
1944            "Bonds": {
1945                "uniques": len(view["stat"]["Bonds"]),
1946                "cost": view["stat"]["bondsCostRUB"],
1947                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1948            },
1949            "Etfs": {
1950                "uniques": len(view["stat"]["Etfs"]),
1951                "cost": view["stat"]["etfsCostRUB"],
1952                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1953            },
1954            "Futures": {
1955                "uniques": len(view["stat"]["Futures"]),
1956                "cost": view["stat"]["futuresCostRUB"],
1957                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1958            },
1959        }
1960
1961        # portfolio distribution by companies:
1962        view["analytics"]["distrByCompanies"]["All money cash"] = {
1963            "ticker": "",
1964            "cost": view["stat"]["allCurrenciesCostRUB"],
1965            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1966        }
1967        view["analytics"]["distrByCompanies"].update(byComp)
1968
1969        # portfolio distribution by sectors:
1970        view["analytics"]["distrBySectors"]["All money cash"] = {
1971            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1972            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1973        }
1974        view["analytics"]["distrBySectors"].update(bySect)
1975
1976        # portfolio distribution by currencies:
1977        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1978            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1979
1980            if self.moreDebug:
1981                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1982
1983        view["analytics"]["distrByCurrencies"].update(byCurr)
1984        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1985        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1986
1987        # portfolio distribution by countries:
1988        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1989            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1990
1991            if self.moreDebug:
1992                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1993
1994        view["analytics"]["distrByCountries"].update(byCountry)
1995        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1996        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1997
1998        # --- Prepare text statistics overview in human-readable:
1999        if show:
2000            # Whatever the value `details`, header not changes:
2001            info = [
2002                "# Client's portfolio\n\n",
2003                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2004                "* **Account ID:** [{}]\n".format(self.accountId),
2005            ]
2006
2007            if details in ["full", "positions", "digest"]:
2008                info.extend([
2009                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2010                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2011                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2012                        view["stat"]["totalChangesRUB"],
2013                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2014                        view["stat"]["totalChangesPercentRUB"],
2015                    ),
2016                ])
2017
2018            if details in ["full", "positions"]:
2019                info.extend([
2020                    "## Open positions\n\n",
2021                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2022                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2023                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2024                        "{:.2f} ({:.2f}) rub".format(
2025                            view["stat"]["availableRUB"],
2026                            view["stat"]["blockedRUB"],
2027                        )
2028                    )
2029                ])
2030
2031                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2032                    return [
2033                        "|                             |                                 |          |              |              |                     |                              |\n",
2034                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2035                            noTradeStr if noTradeStr else typeStr,
2036                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2037                        ),
2038                    ]
2039
2040                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2041                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2042                        "{} [{}]".format(data["ticker"], data["figi"]),
2043                        "{:.2f} ({:.2f}) {}".format(
2044                            data["volume"],
2045                            data["blocked"],
2046                            data["currency"],
2047                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2048                            data["volume"],
2049                            data["blocked"],
2050                        ),
2051                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2052                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2053                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2054                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2055                        "{}{:.2f} {} ({}{:.2f}%)".format(
2056                            "+" if data["profit"] > 0 else "",
2057                            data["profit"], data["baseCurrencyName"],
2058                            "+" if data["percentProfit"] > 0 else "",
2059                            data["percentProfit"],
2060                        ),
2061                    )
2062
2063                # --- Show currencies section:
2064                if view["stat"]["Currencies"]:
2065                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2066                    for item in view["stat"]["Currencies"]:
2067                        info.append(_InfoStr(item, showCurrencyName=True))
2068
2069                else:
2070                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2071
2072                # --- Show shares section:
2073                if view["stat"]["Shares"]:
2074                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2075
2076                    for item in view["stat"]["Shares"]:
2077                        info.append(_InfoStr(item))
2078
2079                else:
2080                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2081
2082                # --- Show bonds section:
2083                if view["stat"]["Bonds"]:
2084                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2085
2086                    for item in view["stat"]["Bonds"]:
2087                        info.append(_InfoStr(item))
2088
2089                else:
2090                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2091
2092                # --- Show etfs section:
2093                if view["stat"]["Etfs"]:
2094                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2095
2096                    for item in view["stat"]["Etfs"]:
2097                        info.append(_InfoStr(item))
2098
2099                else:
2100                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2101
2102                # --- Show futures section:
2103                if view["stat"]["Futures"]:
2104                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2105
2106                    for item in view["stat"]["Futures"]:
2107                        info.append(_InfoStr(item))
2108
2109                else:
2110                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2111
2112            if details in ["full", "orders"]:
2113                # --- Show pending limit orders section:
2114                if view["stat"]["orders"]:
2115                    info.extend([
2116                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2117                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2118                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2119                    ])
2120
2121                    for item in view["stat"]["orders"]:
2122                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2123                            "{} [{}]".format(item["ticker"], item["figi"]),
2124                            item["orderID"],
2125                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2126                            "{} {} ({}{:.2f}%)".format(
2127                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2128                                item["baseCurrencyName"],
2129                                "+" if item["percentChanges"] > 0 else "",
2130                                float(item["percentChanges"]),
2131                            ),
2132                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2133                            item["action"],
2134                            item["type"],
2135                            item["date"],
2136                        ))
2137
2138                else:
2139                    info.append("\n## Total pending limit-orders: 0\n")
2140
2141                # --- Show stop orders section:
2142                if view["stat"]["stopOrders"]:
2143                    info.extend([
2144                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2145                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2146                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2147                    ])
2148
2149                    for item in view["stat"]["stopOrders"]:
2150                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2151                            "{} [{}]".format(item["ticker"], item["figi"]),
2152                            item["orderID"],
2153                            item["lotsRequested"],
2154                            "{} {} ({}{:.2f}%)".format(
2155                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2156                                item["baseCurrencyName"],
2157                                "+" if item["percentChanges"] > 0 else "",
2158                                float(item["percentChanges"]),
2159                            ),
2160                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2161                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2162                            item["action"],
2163                            item["type"],
2164                            item["expType"],
2165                            item["createDate"],
2166                            item["expDate"],
2167                        ))
2168
2169                else:
2170                    info.append("\n## Total stop-orders: 0\n")
2171
2172            if details in ["full", "analytics"]:
2173                # -- Show analytics section:
2174                if view["stat"]["portfolioCostRUB"] > 0:
2175                    info.extend([
2176                        "\n# Analytics\n"
2177                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2178                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2179                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2180                            view["stat"]["totalChangesRUB"],
2181                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2182                            view["stat"]["totalChangesPercentRUB"],
2183                        ),
2184                        "\n## Portfolio distribution by assets\n"
2185                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2186                        "|------------------------------------|---------|---------|--------------------|\n",
2187                    ])
2188
2189                    for key in view["analytics"]["distrByAssets"].keys():
2190                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2191                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2192                                key,
2193                                view["analytics"]["distrByAssets"][key]["uniques"],
2194                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2195                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2196                            ))
2197
2198                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2199
2200                    info.extend([
2201                        "\n## Portfolio distribution by companies\n"
2202                        "\n| Company                                      | Percent | Current cost       |\n",
2203                        aSepLine,
2204                    ])
2205
2206                    for company in view["analytics"]["distrByCompanies"].keys():
2207                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2208                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2209                                "{}{}".format(
2210                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2211                                    company,
2212                                ),
2213                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2214                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2215                            ))
2216
2217                    info.extend([
2218                        "\n## Portfolio distribution by sectors\n"
2219                        "\n| Sector                                       | Percent | Current cost       |\n",
2220                        aSepLine,
2221                    ])
2222
2223                    for sector in view["analytics"]["distrBySectors"].keys():
2224                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2225                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2226                                sector,
2227                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2228                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2229                            ))
2230
2231                    info.extend([
2232                        "\n## Portfolio distribution by currencies\n"
2233                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2234                        aSepLine,
2235                    ])
2236
2237                    for curr in view["analytics"]["distrByCurrencies"].keys():
2238                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2239                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2240                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2241                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2242                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2243                            ))
2244
2245                    info.extend([
2246                        "\n## Portfolio distribution by countries\n"
2247                        "\n| Assets by country                            | Percent | Current cost       |\n",
2248                        aSepLine,
2249                    ])
2250
2251                    for country in view["analytics"]["distrByCountries"].keys():
2252                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2253                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2254                                country,
2255                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2256                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2257                            ))
2258
2259            if details in ["full", "calendar"]:
2260                # -- Show bonds payment calendar section:
2261                if view["stat"]["Bonds"]:
2262                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2263                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2264                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2265
2266                else:
2267                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2268
2269            infoText = "".join(info)
2270
2271            uLogger.info(infoText)
2272
2273            if details == "full" and self.overviewFile:
2274                filename = self.overviewFile
2275
2276            elif details == "digest" and self.overviewDigestFile:
2277                filename = self.overviewDigestFile
2278
2279            elif details == "positions" and self.overviewPositionsFile:
2280                filename = self.overviewPositionsFile
2281
2282            elif details == "orders" and self.overviewOrdersFile:
2283                filename = self.overviewOrdersFile
2284
2285            elif details == "analytics" and self.overviewAnalyticsFile:
2286                filename = self.overviewAnalyticsFile
2287
2288            elif details == "calendar" and self.overviewBondsCalendarFile:
2289                filename = self.overviewBondsCalendarFile
2290
2291            else:
2292                filename = ""
2293
2294            if filename:
2295                with open(filename, "w", encoding="UTF-8") as fH:
2296                    fH.write(infoText)
2297
2298                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2299
2300        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2302    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2303        """
2304        Returns history operations between two given dates for current `accountId`.
2305        If `reportFile` string is not empty then also save human-readable report.
2306        Shows some statistical data of closed positions.
2307
2308        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2309        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2310        :param show: if `True` then also prints all records to the console.
2311        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2312        :return: original list of dictionaries with history of deals records from API ("operations" key):
2313                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2314                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2315        """
2316        if self.accountId is None or not self.accountId:
2317            uLogger.error("Variable `accountId` must be defined for using this method!")
2318            raise Exception("Account ID required")
2319
2320        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2321
2322        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2323
2324        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2325        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2326        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2327        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2328        customStat = {}  # custom statistics in additional to responseJSON
2329
2330        # --- output report in human-readable format:
2331        if show or self.reportFile:
2332            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2333            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2334            nextDay = ""
2335
2336            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2337
2338            if len(ops) > 0:
2339                customStat = {
2340                    "opsCount": 0,  # total operations count
2341                    "buyCount": 0,  # buy operations
2342                    "sellCount": 0,  # sell operations
2343                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2344                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2345                    "payIn": {"rub": 0.},  # Deposit brokerage account
2346                    "payOut": {"rub": 0.},  # Withdrawals
2347                    "divs": {"rub": 0.},  # Dividends income
2348                    "coupons": {"rub": 0.},  # Coupon's income
2349                    "brokerCom": {"rub": 0.},  # Service commissions
2350                    "serviceCom": {"rub": 0.},  # Service commissions
2351                    "marginCom": {"rub": 0.},  # Margin commissions
2352                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2353                }
2354
2355                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2356                for item in ops:
2357                    if item["state"] == "OPERATION_STATE_EXECUTED":
2358                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2359
2360                        # count buy operations:
2361                        if "_BUY" in item["operationType"]:
2362                            customStat["buyCount"] += 1
2363
2364                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2365                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2366
2367                            else:
2368                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2369
2370                        # count sell operations:
2371                        elif "_SELL" in item["operationType"]:
2372                            customStat["sellCount"] += 1
2373
2374                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2375                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2376
2377                            else:
2378                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2379
2380                        # count incoming operations:
2381                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2382                            if item["payment"]["currency"] in customStat["payIn"].keys():
2383                                customStat["payIn"][item["payment"]["currency"]] += payment
2384
2385                            else:
2386                                customStat["payIn"][item["payment"]["currency"]] = payment
2387
2388                        # count withdrawals operations:
2389                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2390                            if item["payment"]["currency"] in customStat["payOut"].keys():
2391                                customStat["payOut"][item["payment"]["currency"]] += payment
2392
2393                            else:
2394                                customStat["payOut"][item["payment"]["currency"]] = payment
2395
2396                        # count dividends income:
2397                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2398                            if item["payment"]["currency"] in customStat["divs"].keys():
2399                                customStat["divs"][item["payment"]["currency"]] += payment
2400
2401                            else:
2402                                customStat["divs"][item["payment"]["currency"]] = payment
2403
2404                        # count coupon's income:
2405                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2406                            if item["payment"]["currency"] in customStat["coupons"].keys():
2407                                customStat["coupons"][item["payment"]["currency"]] += payment
2408
2409                            else:
2410                                customStat["coupons"][item["payment"]["currency"]] = payment
2411
2412                        # count broker commissions:
2413                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2414                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2415                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2416
2417                            else:
2418                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2419
2420                        # count service commissions:
2421                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2422                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2423                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2424
2425                            else:
2426                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2427
2428                        # count margin commissions:
2429                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2430                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2431                                customStat["marginCom"][item["payment"]["currency"]] += payment
2432
2433                            else:
2434                                customStat["marginCom"][item["payment"]["currency"]] = payment
2435
2436                        # count withholding taxes:
2437                        elif "_TAX" in item["operationType"]:
2438                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2439                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2440
2441                            else:
2442                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2443
2444                        else:
2445                            continue
2446
2447                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2448
2449                # --- view "Actions" lines:
2450                info.extend([
2451                    "| Report sections            |                               |                              |                      |                        |\n",
2452                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2453                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2454                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2455                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2456                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2457                    ),
2458                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2459                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2460                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2461                    ),
2462                ])
2463
2464                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2465                for key in opsKeys:
2466                    if key == "rub":
2467                        continue
2468
2469                    info.extend([
2470                        "|                            |                               | {:<28} |                      |                        |\n".format(
2471                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2472                        ),
2473                        "|                            |                               | {:<28} |                      |                        |\n".format(
2474                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2475                        ),
2476                    ])
2477
2478                info.append(splitLine1)
2479
2480                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2481                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2482                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2483                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2484                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2485                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2486                    )
2487
2488                # --- view "Payments" lines:
2489                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2490                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2491
2492                for key in paymentsKeys:
2493                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2494
2495                info.append(splitLine1)
2496
2497                # --- view "Commissions and taxes" lines:
2498                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2499                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2500
2501                for key in comKeys:
2502                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2503
2504                info.append(splitLine1)
2505
2506                info.extend([
2507                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2508                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2509                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2510                ])
2511
2512            else:
2513                info.append("Broker returned no operations during this period\n")
2514
2515            # --- view "Operations" section:
2516            for item in ops:
2517                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2518                    continue
2519
2520                else:
2521                    self.figi = item["figi"] if item["figi"] else ""
2522                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2523                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2524
2525                    # group of deals during one day:
2526                    if nextDay and item["date"].split("T")[0] != nextDay:
2527                        info.append(splitLine2)
2528                        nextDay = ""
2529
2530                    else:
2531                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2532
2533                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2534                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2535                        self.figi if self.figi else "—",
2536                        instrument["ticker"] if instrument else "—",
2537                        instrument["type"] if instrument else "—",
2538                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2539                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2540                        TKS_OPERATION_STATES[item["state"]],
2541                        TKS_OPERATION_TYPES[item["operationType"]],
2542                    ))
2543
2544            infoText = "".join(info)
2545
2546            if show:
2547                if self.moreDebug:
2548                    uLogger.debug("Records about history of a client's operations successfully received")
2549
2550                uLogger.info(infoText)
2551
2552            if self.reportFile:
2553                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2554                    fH.write(infoText)
2555
2556                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2557
2558        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2560    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2561        """
2562        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2563
2564        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2565        Warning! Broker server used ISO UTC time by default.
2566
2567        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2568        Also, `historyFile` used to update history with `onlyMissing` parameter.
2569
2570        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2571
2572        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2573        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2574        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2575                         `"hour"`, `"day"`. Default: `"hour"`.
2576        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2577                            False by default. Warning! History appends only from last candle to current time
2578                            with always update last candle!
2579        :param csvSep: separator if csv-file is used, `,` by default.
2580        :param show: if `True` then also prints Pandas DataFrame to the console.
2581        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2582                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2583        """
2584        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2585        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2586        history = None  # empty pandas object for history
2587
2588        if interval not in TKS_CANDLE_INTERVALS.keys():
2589            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2590            raise Exception("Incorrect value")
2591
2592        if not (self.ticker or self.figi):
2593            uLogger.error("Ticker or FIGI must be defined!")
2594            raise Exception("Ticker or FIGI required")
2595
2596        if self.ticker and not self.figi:
2597            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2598            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2599
2600        if self.figi and not self.ticker:
2601            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2602            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2603
2604        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2605        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2606        if interval.lower() != "day":
2607            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2608
2609        delta = dtEnd - dtStart  # current UTC time minus last time in file
2610        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2611
2612        # calculate history length in candles:
2613        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2614        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2615            length += 1  # to avoid fraction time
2616
2617        # calculate data blocks count:
2618        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2619
2620        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2621        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2622        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2623        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2624        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2625
2626        tempOld = None  # pandas object for old history, if --only-missing key present
2627        lastTime = None  # datetime object of last old candle in file
2628
2629        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2630            uLogger.debug("--only-missing key present, add only last missing candles...")
2631            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2632
2633            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2634
2635            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2636            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2637            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2638            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2639
2640            # get last datetime object from last string in file or minus 1 delta if file is empty:
2641            if len(tempOld) > 0:
2642                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2643
2644            else:
2645                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2646
2647            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2648
2649        responseJSONs = []  # raw history blocks of data
2650
2651        blockEnd = dtEnd
2652        for item in range(blocks):
2653            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2654            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2655
2656            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2657                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2658            ))
2659
2660            if blockStart == blockEnd:
2661                uLogger.debug("Skipped this zero-length block...")
2662
2663            else:
2664                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2665                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2666                self.body = str({
2667                    "figi": self.figi,
2668                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2669                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2670                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2671                })
2672                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2673
2674                if "code" in responseJSON.keys():
2675                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2676
2677                else:
2678                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2679                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2680
2681                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2682
2683            blockEnd = blockStart
2684
2685        printCount = len(responseJSONs)  # candles to show in console
2686        if responseJSONs:
2687            tempHistory = pd.DataFrame(
2688                data={
2689                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2690                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2691                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2692                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2693                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2694                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2695                    "volume": [int(item["volume"]) for item in responseJSONs],
2696                },
2697                index=range(len(responseJSONs)),
2698                columns=["date", "time", "open", "high", "low", "close", "volume"],
2699            )
2700            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2701            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2702
2703            # append only newest candles to old history if --only-missing key present:
2704            if onlyMissing and tempOld is not None and lastTime is not None:
2705                index = 0  # find start index in tempHistory data:
2706
2707                for i, item in tempHistory.iterrows():
2708                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2709
2710                    if curTime == lastTime:
2711                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2712                        index = i
2713                        printCount = index + 1
2714                        break
2715
2716                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2717
2718            else:
2719                history = tempHistory  # if no `--only-missing` key then load full data from server
2720
2721            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2722
2723        if history is not None and not history.empty:
2724            if show:
2725                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2726                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2727                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2728                ))
2729
2730        else:
2731            uLogger.warning("Received an empty candles history!")
2732
2733        if self.historyFile is not None:
2734            if history is not None and not history.empty:
2735                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2736                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2737
2738            else:
2739                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2740
2741        else:
2742            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2743
2744        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2746    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2747        """
2748        Load candles history from csv-file and return Pandas DataFrame object.
2749
2750        See also: `History()` and `ShowHistoryChart()` methods.
2751
2752        :param filePath: path to csv-file to open.
2753        """
2754        loadedHistory = None  # init candles data object
2755
2756        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2757
2758        if os.path.exists(filePath):
2759            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2760
2761            tfStr = self.priceModel.FormattedDelta(
2762                self.priceModel.timeframe,
2763                "{days} days {hours}h {minutes}m {seconds}s",
2764            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2765                self.priceModel.timeframe,
2766                "{hours}h {minutes}m {seconds}s",
2767            )
2768
2769            if loadedHistory is not None and not loadedHistory.empty:
2770                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2771                    len(loadedHistory),
2772                    tfStr,
2773                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2774                )
2775
2776            else:
2777                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2778
2779        else:
2780            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2781
2782        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2784    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2785        """
2786        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2787
2788        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2789        Default: `index.html` (both for interact and non-interact candlesticks chart).
2790
2791        See also: `History()` and `LoadHistory()` methods.
2792
2793        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2794        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2795                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2796                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2797                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2798        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2799                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2800        """
2801        if isinstance(candles, str):
2802            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2803            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2804
2805        elif isinstance(candles, pd.DataFrame):
2806            self.priceModel.prices = candles  # set candles chain from variable
2807            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2808
2809            if "datetime" not in candles.columns:
2810                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2811
2812        else:
2813            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2814            raise Exception("Incorrect value")
2815
2816        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2817
2818        if interact:
2819            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2820
2821            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2822
2823        else:
2824            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2825
2826            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2827
2828        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2830    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2831        """
2832        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2833        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2834
2835        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2836
2837        :param operation: string "Buy" or "Sell".
2838        :param lots: volume, integer count of lots >= 1.
2839        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2840        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2841        :param expDate: string "Undefined" by default or local date in future,
2842                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2843        :return: JSON with response from broker server.
2844        """
2845        if self.accountId is None or not self.accountId:
2846            uLogger.error("Variable `accountId` must be defined for using this method!")
2847            raise Exception("Account ID required")
2848
2849        if operation is None or not operation or operation not in ("Buy", "Sell"):
2850            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2851            raise Exception("Incorrect value")
2852
2853        if lots is None or lots < 1:
2854            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2855            lots = 1
2856
2857        if tp is None or tp < 0:
2858            tp = 0
2859
2860        if sl is None or sl < 0:
2861            sl = 0
2862
2863        if expDate is None or not expDate:
2864            expDate = "Undefined"
2865
2866        if not (self.ticker or self.figi):
2867            uLogger.error("Ticker or FIGI must be defined!")
2868            raise Exception("Ticker or FIGI required")
2869
2870        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2871        self.ticker = instrument["ticker"]
2872        self.figi = instrument["figi"]
2873
2874        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2875
2876        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2877        self.body = str({
2878            "figi": self.figi,
2879            "quantity": str(lots),
2880            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2881            "accountId": str(self.accountId),
2882            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2883        })
2884        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2885
2886        if "orderId" in response.keys():
2887            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2888                operation, response["orderId"],
2889                self.ticker, self.figi, lots,
2890                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2891                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2892                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2893            ))
2894
2895            if tp > 0:
2896                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2897
2898            if sl > 0:
2899                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2900
2901        else:
2902            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2903
2904        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2906    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2907        """
2908        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2909        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2910
2911        See also: `Order()` and `Trade()` docstrings.
2912
2913        :param lots: volume, integer count of lots >= 1.
2914        :param tp: float > 0, take profit price of stop-order.
2915        :param sl: float > 0, stop loss price of stop-order.
2916        :param expDate: it's a local date in future.
2917                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2918        :return: JSON with response from broker server.
2919        """
2920        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2922    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2923        """
2924        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2925        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2926
2927        See also: `Order()` and `Trade()` docstrings.
2928
2929        :param lots: volume, integer count of lots >= 1.
2930        :param tp: float > 0, take profit price of stop-order.
2931        :param sl: float > 0, stop loss price of stop-order.
2932        :param expDate: it's a local date in the future.
2933                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2934        :return: JSON with response from broker server.
2935        """
2936        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2938    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2939        """
2940        Close position of given instruments.
2941
2942        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2943        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2944                         This avoids unnecessary downloading data from the server.
2945        """
2946        if instruments is None or not instruments:
2947            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2948            raise Exception("Ticker or FIGI required")
2949
2950        if isinstance(instruments, str):
2951            instruments = [instruments]
2952
2953        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2954        if uniqueInstruments:
2955            if portfolio is None or not portfolio:
2956                portfolio = self.Overview(show=False)
2957
2958            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2959            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2960
2961            for self.figi in uniqueInstruments:
2962                if self.figi not in allOpened:
2963                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2964                    continue
2965
2966                # search open trade info about instrument by ticker:
2967                instrument = {}
2968                for iType in TKS_INSTRUMENTS:
2969                    if instrument:
2970                        break
2971
2972                    for item in portfolio["stat"][iType]:
2973                        if item["figi"] == self.figi:
2974                            instrument = item
2975                            break
2976
2977                if instrument:
2978                    self.ticker = instrument["ticker"]
2979                    self.figi = instrument["figi"]
2980
2981                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2982                        self.ticker,
2983                        self.figi,
2984                        int(instrument["volume"]),
2985                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2986                    ))
2987
2988                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2989
2990                    if tradeLots > 0:
2991                        if instrument["blocked"] > 0:
2992                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2993                                instrument["blocked"],
2994                                self.ticker,
2995                                tradeLots,
2996                            ))
2997
2998                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2999                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3000
3001                    else:
3002                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3004    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3005        """
3006        Close all positions of given instruments with defined type.
3007
3008        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3009        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3010                         This avoids unnecessary downloading data from the server.
3011        """
3012        if iType not in TKS_INSTRUMENTS:
3013            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3014
3015        else:
3016            if portfolio is None or not portfolio:
3017                portfolio = self.Overview(show=False)
3018
3019            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3020            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3021
3022            if tickers and portfolio:
3023                self.CloseTrades(tickers, portfolio)
3024
3025            else:
3026                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3028    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3029        """
3030        Universal method to create market or limit orders with all available parameters for current `accountId`.
3031        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3032
3033        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3034        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3035
3036        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3037        then broker immediately open market order as you can do simple --buy or --sell operations!
3038
3039        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3040        When current price will go up or down to target price value then broker opens a limit order.
3041        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3042
3043        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3044
3045        :param operation: string "Buy" or "Sell".
3046        :param orderType: string "Limit" or "Stop".
3047        :param lots: volume, integer count of lots >= 1.
3048        :param targetPrice: target price > 0. This is open trade price for limit order.
3049        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3050                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3051        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3052                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3053                         Stop loss order always executed by market price.
3054        :param expDate: string "Undefined" by default or local date in future.
3055                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3056                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3057                        A limit order has no expiration date, it lasts until the end of the trading day.
3058        :return: JSON with response from broker server.
3059        """
3060        if self.accountId is None or not self.accountId:
3061            uLogger.error("Variable `accountId` must be defined for using this method!")
3062            raise Exception("Account ID required")
3063
3064        if operation is None or not operation or operation not in ("Buy", "Sell"):
3065            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3066            raise Exception("Incorrect value")
3067
3068        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3069            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3070            raise Exception("Incorrect value")
3071
3072        if lots is None or lots < 1:
3073            uLogger.error("You must define trade volume > 0: integer count of lots!")
3074            raise Exception("Incorrect value")
3075
3076        if targetPrice is None or targetPrice <= 0:
3077            uLogger.error("Target price for limit-order must be greater than 0!")
3078            raise Exception("Incorrect value")
3079
3080        if limitPrice is None or limitPrice <= 0:
3081            limitPrice = targetPrice
3082
3083        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3084            stopType = "Limit"
3085
3086        if expDate is None or not expDate:
3087            expDate = "Undefined"
3088
3089        if not (self.ticker or self.figi):
3090            uLogger.error("Tocker or FIGI must be defined!")
3091            raise Exception("Ticker or FIGI required")
3092
3093        response = {}
3094        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3095        self.ticker = instrument["ticker"]
3096        self.figi = instrument["figi"]
3097
3098        if orderType == "Limit":
3099            uLogger.debug(
3100                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3101                    self.ticker, self.figi,
3102                    operation, lots, targetPrice, instrument["currency"],
3103                ))
3104
3105            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3106            self.body = str({
3107                "figi": self.figi,
3108                "quantity": str(lots),
3109                "price": FloatToNano(targetPrice),
3110                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3111                "accountId": str(self.accountId),
3112                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3113            })
3114            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3115
3116            if "orderId" in response.keys():
3117                uLogger.info(
3118                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3119                        response["orderId"],
3120                        self.ticker, self.figi,
3121                        operation, lots, targetPrice, instrument["currency"],
3122                    ))
3123
3124                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3125                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3126                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3127                            targetPrice, instrument["currency"],
3128                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3129                        ))
3130
3131                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3132                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3133                            targetPrice, instrument["currency"],
3134                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3135                        ))
3136
3137            else:
3138                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3139
3140        if orderType == "Stop":
3141            uLogger.debug(
3142                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3143                    self.ticker, self.figi,
3144                    operation, lots,
3145                    targetPrice, instrument["currency"],
3146                    limitPrice, instrument["currency"],
3147                    stopType, expDate,
3148                ))
3149
3150            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3151            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3152            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3153
3154            body = {
3155                "figi": self.figi,
3156                "quantity": str(lots),
3157                "price": FloatToNano(limitPrice),
3158                "stopPrice": FloatToNano(targetPrice),
3159                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3160                "accountId": str(self.accountId),
3161                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3162                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3163            }
3164
3165            if expDateUTC:
3166                body["expireDate"] = expDateUTC
3167
3168            self.body = str(body)
3169            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3170
3171            if "stopOrderId" in response.keys():
3172                uLogger.info(
3173                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3174                        response["stopOrderId"],
3175                        self.ticker, self.figi,
3176                        operation, lots,
3177                        targetPrice, instrument["currency"],
3178                        limitPrice, instrument["currency"],
3179                        TKS_STOP_ORDER_TYPES[stopOrderType],
3180                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3181                    ))
3182
3183                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3184                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3185                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3186                            targetPrice, instrument["currency"],
3187                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3188                        ))
3189
3190                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3191                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3192                            targetPrice, instrument["currency"],
3193                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3194                        ))
3195
3196            else:
3197                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3198
3199        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3201    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3202        """
3203        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3204        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3205        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3206        See also: `Order()` docstring.
3207
3208        :param lots: volume, integer count of lots >= 1.
3209        :param targetPrice: target price > 0. This is open trade price for limit order.
3210        :return: JSON with response from broker server.
3211        """
3212        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3214    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3215        """
3216        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3217        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3218        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3219        target price value then broker opens a limit order. See also: `Order()` docstring.
3220
3221        :param lots: volume, integer count of lots >= 1.
3222        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3223        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3224                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3225        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3226                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3227        :param expDate: string "Undefined" by default or local date in future.
3228                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3229                        This date is converting to UTC format for server.
3230        :return: JSON with response from broker server.
3231        """
3232        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3234    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3235        """
3236        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3237        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3238        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3239        See also: `Order()` docstring.
3240
3241        :param lots: volume, integer count of lots >= 1.
3242        :param targetPrice: target price > 0. This is open trade price for limit order.
3243        :return: JSON with response from broker server.
3244        """
3245        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3247    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3248        """
3249        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3250        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3251        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3252        target price value then broker opens a limit order. See also: `Order()` docstring.
3253
3254        :param lots: volume, integer count of lots >= 1.
3255        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3256        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3257                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3258        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3259                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3260        :param expDate: string "Undefined" by default or local date in future.
3261                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3262                        This date is converting to UTC format for server.
3263        :return: JSON with response from broker server.
3264        """
3265        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3267    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3268        """
3269        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3270
3271        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3272        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3273                             This avoids unnecessary downloading data from the server.
3274        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3275        """
3276        if self.accountId is None or not self.accountId:
3277            uLogger.error("Variable `accountId` must be defined for using this method!")
3278            raise Exception("Account ID required")
3279
3280        if orderIDs:
3281            if allOrdersIDs is None:
3282                rawOrders = self.RequestPendingOrders()
3283                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3284
3285            if allStopOrdersIDs is None:
3286                rawStopOrders = self.RequestStopOrders()
3287                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3288
3289            for orderID in orderIDs:
3290                idInPendingOrders = orderID in allOrdersIDs
3291                idInStopOrders = orderID in allStopOrdersIDs
3292
3293                if not (idInPendingOrders or idInStopOrders):
3294                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3295                    continue
3296
3297                else:
3298                    if idInPendingOrders:
3299                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3300
3301                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3302                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3303                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3304                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3305
3306                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3307                            if self.moreDebug:
3308                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3309
3310                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3311
3312                        else:
3313                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3314
3315                    elif idInStopOrders:
3316                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3317
3318                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3319                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3320                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3321                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3322
3323                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3324                            if self.moreDebug:
3325                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3326
3327                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3328
3329                        else:
3330                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3331
3332                    else:
3333                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3335    def CloseAllOrders(self) -> None:
3336        """
3337        Gets a list of open pending and stop orders and cancel it all.
3338        """
3339        rawOrders = self.RequestPendingOrders()
3340        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3341        lenOrders = len(allOrdersIDs)
3342
3343        rawStopOrders = self.RequestStopOrders()
3344        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3345        lenSOrders = len(allStopOrdersIDs)
3346
3347        if lenOrders > 0 or lenSOrders > 0:
3348            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3349
3350            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3351
3352        else:
3353            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3355    def CloseAll(self, *args) -> None:
3356        """
3357        Close all available (not blocked) opened trades and orders.
3358
3359        Also, you can select one or more keywords case-insensitive:
3360        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3361
3362        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3363        """
3364        overview = self.Overview(show=False)  # get all open trades info
3365
3366        if len(args) == 0:
3367            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3368            self.CloseAllOrders()  # close all pending and stop orders
3369
3370            for iType in TKS_INSTRUMENTS:
3371                if iType != "Currencies":
3372                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3373
3374        else:
3375            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3376            lowerArgs = [x.lower() for x in args]
3377
3378            if "orders" in lowerArgs:
3379                self.CloseAllOrders()  # close all pending and stop orders
3380
3381            for iType in TKS_INSTRUMENTS:
3382                if iType.lower() in lowerArgs and iType != "Currencies":
3383                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3385    def CloseAllByTicker(self, instrument: str) -> None:
3386        """
3387        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3388
3389        This method searches opened trade and orders of instrument throw all portfolio and then use
3390        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3391
3392        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3393
3394        :param instrument: string with ticker.
3395        """
3396        if instrument is None or not instrument:
3397            uLogger.error("Ticker name must be defined for using this method!")
3398            raise Exception("Ticker required")
3399
3400        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3401
3402        self.ticker = instrument  # try to set instrument as ticker
3403        self.figi = ""
3404
3405        if self.IsInPortfolio(portfolio=overview):
3406            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3407            self.CloseTrades(instruments=[instrument], portfolio=overview)
3408
3409        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3410        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3411
3412        if limitAll and self.IsInLimitOrders(portfolio=overview):
3413            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3414            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3415
3416        if stopAll and self.IsInStopOrders(portfolio=overview):
3417            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3418            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3420    def CloseAllByFIGI(self, instrument: str) -> None:
3421        """
3422        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3423
3424        This method searches opened trade and orders of instrument throw all portfolio and then use
3425        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3426
3427        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3428
3429        :param instrument: string with FIGI id.
3430        """
3431        if instrument is None or not instrument:
3432            uLogger.error("FIGI id must be defined for using this method!")
3433            raise Exception("FIGI required")
3434
3435        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3436
3437        self.ticker = ""
3438        self.figi = instrument  # try to set instrument as FIGI id
3439
3440        if self.IsInPortfolio(portfolio=overview):
3441            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3442            self.CloseTrades(instruments=[instrument], portfolio=overview)
3443
3444        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3445        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3446
3447        if limitAll and self.IsInLimitOrders(portfolio=overview):
3448            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3449            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3450
3451        if stopAll and self.IsInStopOrders(portfolio=overview):
3452            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3453            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3455    @staticmethod
3456    def ParseOrderParameters(operation, **inputParameters):
3457        """
3458        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3459
3460        :param operation: string "Buy" or "Sell".
3461        :param inputParameters: this is dict of strings that looks like this
3462               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3463               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3464               "prices" key: one or more prices to open limit-orders
3465               Counts of values in lots and prices lists must be equals!
3466        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3467        """
3468        # TODO: update order grid work with api v2
3469        pass
3470        # uLogger.debug("Input parameters: {}".format(inputParameters))
3471        #
3472        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3473        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3474        #     raise Exception("Incorrect value")
3475        #
3476        # if "l" in inputParameters.keys():
3477        #     inputParameters["lots"] = inputParameters.pop("l")
3478        #
3479        # if "p" in inputParameters.keys():
3480        #     inputParameters["prices"] = inputParameters.pop("p")
3481        #
3482        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3483        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3484        #     raise Exception("Incorrect value")
3485        #
3486        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3487        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3488        #
3489        # if len(lots) != len(prices):
3490        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3491        #     raise Exception("Incorrect value")
3492        #
3493        # uLogger.debug("Extracted parameters for orders:")
3494        # uLogger.debug("lots = {}".format(lots))
3495        # uLogger.debug("prices = {}".format(prices))
3496        #
3497        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3498        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3499        # uLogger.debug("Order parameters: {}".format(result))
3500        #
3501        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3503    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3504        """
3505        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3506
3507        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3508        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3509        """
3510        result = False
3511        msg = "Instrument not defined!"
3512
3513        if portfolio is None or not portfolio:
3514            portfolio = self.Overview(show=False)
3515
3516        if self.ticker:
3517            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self.ticker))
3518            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3519
3520            for iType in TKS_INSTRUMENTS:
3521                for instrument in portfolio["stat"][iType]:
3522                    if instrument["ticker"] == self.ticker:
3523                        result = True
3524                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3525                        break
3526
3527        elif self.figi:
3528            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self.figi))
3529            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["figi"] == self.figi:
3534                        result = True
3535                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3536                        break
3537
3538        else:
3539            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3540
3541        uLogger.debug(msg)
3542
3543        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3545    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3546        """
3547        Returns instrument from the user's portfolio if it presents there.
3548        Instrument must be defined by `ticker` (highly priority) or `figi`.
3549
3550        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3551        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3552        """
3553        result = None
3554        msg = "Instrument not defined!"
3555
3556        if portfolio is None or not portfolio:
3557            portfolio = self.Overview(show=False)
3558
3559        if self.ticker:
3560            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3561            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3562
3563            for iType in TKS_INSTRUMENTS:
3564                for instrument in portfolio["stat"][iType]:
3565                    if instrument["ticker"] == self.ticker:
3566                        result = instrument
3567                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3568                        break
3569
3570        elif self.figi:
3571            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3572            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3573
3574            for iType in TKS_INSTRUMENTS:
3575                for instrument in portfolio["stat"][iType]:
3576                    if instrument["figi"] == self.figi:
3577                        result = instrument
3578                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3579                        break
3580
3581        else:
3582            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3583
3584        uLogger.debug(msg)
3585
3586        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3588    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3589        """
3590        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3591
3592        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3593
3594        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3595        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3596        """
3597        result = False
3598        msg = "Instrument not defined!"
3599
3600        if portfolio is None or not portfolio:
3601            portfolio = self.Overview(show=False)
3602
3603        if self.ticker:
3604            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker))
3605            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker)
3606
3607            for instrument in portfolio["stat"]["orders"]:
3608                if instrument["ticker"] == self.ticker:
3609                    result = True
3610                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker)
3611                    break
3612
3613        elif self.figi:
3614            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi))
3615            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi)
3616
3617            for instrument in portfolio["stat"]["orders"]:
3618                if instrument["figi"] == self.figi:
3619                    result = True
3620                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi)
3621                    break
3622
3623        else:
3624            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3625
3626        uLogger.debug(msg)
3627
3628        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3630    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3631        """
3632        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3633        Instrument must be defined by `ticker` (highly priority) or `figi`.
3634
3635        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3636
3637        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3638        :return: list with `orderID`s of limit orders.
3639        """
3640        result = []
3641        msg = "Instrument not defined!"
3642
3643        if portfolio is None or not portfolio:
3644            portfolio = self.Overview(show=False)
3645
3646        if self.ticker:
3647            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self.ticker))
3648            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self.ticker)
3649
3650            for instrument in portfolio["stat"]["orders"]:
3651                if instrument["ticker"] == self.ticker:
3652                    result.append(instrument["orderID"])
3653
3654            if result:
3655                msg = "Instrument with ticker [{}] is present in limit orders list".format(self.ticker)
3656
3657        elif self.figi:
3658            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self.figi))
3659            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self.figi)
3660
3661            for instrument in portfolio["stat"]["orders"]:
3662                if instrument["figi"] == self.figi:
3663                    result.append(instrument["orderID"])
3664
3665            if result:
3666                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self.figi)
3667
3668        else:
3669            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3670
3671        uLogger.debug(msg)
3672
3673        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3675    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3676        """
3677        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3678
3679        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3680
3681        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3682        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3683        """
3684        result = False
3685        msg = "Instrument not defined!"
3686
3687        if portfolio is None or not portfolio:
3688            portfolio = self.Overview(show=False)
3689
3690        if self.ticker:
3691            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker))
3692            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker)
3693
3694            for instrument in portfolio["stat"]["stopOrders"]:
3695                if instrument["ticker"] == self.ticker:
3696                    result = True
3697                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker)
3698                    break
3699
3700        elif self.figi:
3701            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi))
3702            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi)
3703
3704            for instrument in portfolio["stat"]["stopOrders"]:
3705                if instrument["figi"] == self.figi:
3706                    result = True
3707                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi)
3708                    break
3709
3710        else:
3711            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3712
3713        uLogger.debug(msg)
3714
3715        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3717    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3718        """
3719        Returns list with all `orderID`s of opened stop orders for the instrument.
3720        Instrument must be defined by `ticker` (highly priority) or `figi`.
3721
3722        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3723
3724        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3725        :return: list with `orderID`s of stop orders.
3726        """
3727        result = []
3728        msg = "Instrument not defined!"
3729
3730        if portfolio is None or not portfolio:
3731            portfolio = self.Overview(show=False)
3732
3733        if self.ticker:
3734            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self.ticker))
3735            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self.ticker)
3736
3737            for instrument in portfolio["stat"]["stopOrders"]:
3738                if instrument["ticker"] == self.ticker:
3739                    result.append(instrument["orderID"])
3740
3741            if result:
3742                msg = "Instrument with ticker [{}] is present in stop orders list".format(self.ticker)
3743
3744        elif self.figi:
3745            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self.figi))
3746            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self.figi)
3747
3748            for instrument in portfolio["stat"]["stopOrders"]:
3749                if instrument["figi"] == self.figi:
3750                    result.append(instrument["orderID"])
3751
3752            if result:
3753                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self.figi)
3754
3755        else:
3756            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3757
3758        uLogger.debug(msg)
3759
3760        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3762    def RequestLimits(self) -> dict:
3763        """
3764        Method for obtaining the available funds for withdrawal for current `accountId`.
3765
3766        See also:
3767        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3768        - `OverviewLimits()` method
3769
3770        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3771                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3772                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3773                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3774        """
3775        if self.accountId is None or not self.accountId:
3776            uLogger.error("Variable `accountId` must be defined for using this method!")
3777            raise Exception("Account ID required")
3778
3779        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3780
3781        self.body = str({"accountId": self.accountId})
3782        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3783        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3784
3785        if self.moreDebug:
3786            uLogger.debug("Records about available funds for withdrawal successfully received")
3787
3788        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3790    def OverviewLimits(self, show: bool = False) -> dict:
3791        """
3792        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3793
3794        See also: `RequestLimits()`.
3795
3796        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3797        :return: dict with raw parsed data from server and some calculated statistics about it.
3798        """
3799        if self.accountId is None or not self.accountId:
3800            uLogger.error("Variable `accountId` must be defined for using this method!")
3801            raise Exception("Account ID required")
3802
3803        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3804
3805        view = {
3806            "rawLimits": rawLimits,
3807            "limits": {  # parsed data for every currency:
3808                "money": {  # this is an array of portfolio currency positions
3809                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3810                },
3811                "blocked": {  # this is an array of blocked currency
3812                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3813                },
3814                "blockedGuarantee": {  # this is locked money under collateral for futures
3815                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3816                },
3817            },
3818        }
3819
3820        # --- Prepare text table with limits in human-readable format:
3821        if show:
3822            info = [
3823                "# Withdrawal limits\n\n",
3824                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3825                "* **Account ID:** [{}]\n".format(self.accountId),
3826            ]
3827
3828            if view["limits"]["money"]:
3829                info.extend([
3830                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3831                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3832                ])
3833
3834            else:
3835                info.append("\nNo withdrawal limits\n")
3836
3837            for curr in view["limits"]["money"].keys():
3838                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3839                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3840                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3841
3842                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3843                    "[{}]".format(curr),
3844                    "{:.2f}".format(view["limits"]["money"][curr]),
3845                    "{:.2f}".format(availableMoney),
3846                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3847                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3848                )
3849
3850                if curr == "rub":
3851                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3852
3853                else:
3854                    info.append(infoStr)
3855
3856            infoText = "".join(info)
3857
3858            uLogger.info(infoText)
3859
3860            if self.withdrawalLimitsFile:
3861                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3862                    fH.write(infoText)
3863
3864                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3865
3866        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3868    def RequestAccounts(self) -> dict:
3869        """
3870        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3871
3872        See also:
3873        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3874        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3875        - `OverviewUserInfo()` method
3876
3877        :return: dict with raw data from server that contains accounts info. Example of dict:
3878                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3879                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3880                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3881                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3882        """
3883        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3884
3885        self.body = str({})
3886        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3887        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3888
3889        if self.moreDebug:
3890            uLogger.debug("Records about available accounts successfully received")
3891
3892        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3894    def RequestUserInfo(self) -> dict:
3895        """
3896        Method for requesting common user's information.
3897
3898        See also:
3899        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3900        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3901        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3902        - `OverviewUserInfo()` method
3903
3904        :return: dict with raw data from server that contains user's information. Example of dict:
3905                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3906                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3907        """
3908        uLogger.debug("Requesting common user's information. Wait, please...")
3909
3910        self.body = str({})
3911        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3912        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3913
3914        if self.moreDebug:
3915            uLogger.debug("Records about current user successfully received")
3916
3917        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3919    def RequestMarginStatus(self, accountId: str = None) -> dict:
3920        """
3921        Method for requesting margin calculation for defined account ID.
3922
3923        See also:
3924        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3925        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3926        - `OverviewUserInfo()` method
3927
3928        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3929        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3930                 Example of responses:
3931                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3932                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3933                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3934                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3935                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3936                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3937        """
3938        if accountId is None or not accountId:
3939            if self.accountId is None or not self.accountId:
3940                uLogger.error("Variable `accountId` must be defined for using this method!")
3941                raise Exception("Account ID required")
3942
3943            else:
3944                accountId = self.accountId  # use `self.accountId` (main ID) by default
3945
3946        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3947
3948        self.body = str({"accountId": accountId})
3949        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3950        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3951
3952        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3953            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3954            rawMargin = {}
3955
3956        else:
3957            if self.moreDebug:
3958                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3959
3960        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3962    def RequestTariffLimits(self) -> dict:
3963        """
3964        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3965
3966        See also:
3967        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3968        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3969        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3970        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3971        - `OverviewUserInfo()` method
3972
3973        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3974                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3975                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3976        """
3977        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3978
3979        self.body = str({})
3980        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3981        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3982
3983        if self.moreDebug:
3984            uLogger.debug("Records with limits of current tariff successfully received")
3985
3986        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3988    def RequestBondCoupons(self, iJSON: dict) -> dict:
3989        """
3990        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3991        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3992        All dates are in UTC timezone.
3993
3994        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3995        Documentation:
3996        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3997        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3998
3999        See also: `ExtendBondsData()`.
4000
4001        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
4002                      If raw iJSON is not data of bond then server returns an error [400] with message:
4003                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4004        :return: dictionary with bond payment calendar. Response example
4005                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4006                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4007                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4008                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4009        """
4010        if iJSON["figi"] is None or not iJSON["figi"]:
4011            uLogger.error("FIGI must be defined for using this method!")
4012            raise Exception("FIGI required")
4013
4014        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4015        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4016
4017        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4018            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4019            self.figi,
4020            startDate,
4021            endDate,
4022        ))
4023
4024        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4025        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4026        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4027
4028        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4029            uLogger.warning("Instrument type is not bond!")
4030
4031        else:
4032            if self.moreDebug:
4033                uLogger.debug("Records about bond payment calendar successfully received")
4034
4035        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4037    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4038        """
4039        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4040        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4041        coupon yields, current yields and some statistics etc.
4042
4043        WARNING! This is too long operation if a lot of bonds requested from broker server.
4044
4045        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4046
4047        :param instruments: list of strings with tickers or FIGIs.
4048        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4049                     for further used by data scientists or stock analytics.
4050        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4051                 In XLSX-file and Pandas DataFrame fields mean:
4052                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4053                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4054        """
4055        if instruments is None or not instruments:
4056            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4057            raise Exception("Ticker or FIGI required")
4058
4059        if isinstance(instruments, str):
4060            instruments = [instruments]
4061
4062        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4063
4064        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4065
4066        iCount = len(uniqueInstruments)
4067        tooLong = iCount >= 20
4068        if tooLong:
4069            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4070
4071        bonds = None
4072        for i, self.figi in enumerate(uniqueInstruments):
4073            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4074
4075            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4076                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4077                rawBond = self.SearchByFIGI(requestPrice=True)
4078
4079                # Widen raw data with UTC current time (iData["actualDateTime"]):
4080                actualDate = datetime.now(tzutc())
4081                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4082
4083                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4084                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4085
4086                # Replace some values with human-readable:
4087                iData["nominalCurrency"] = iData["nominal"]["currency"]
4088                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4089                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4090                iData["aciCurrency"] = iData["aciValue"]["currency"]
4091                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4092                iData["issueSize"] = int(iData["issueSize"])
4093                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4094                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4095                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4096                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4097                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4098                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4099                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4100                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4101                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4102                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4103
4104                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4105                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4106                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4107                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4108                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4109                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4110                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4111                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4112                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4113                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4114                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4115
4116                # Widen raw data with calendar data from `rawCalendar` values:
4117                calendarData = []
4118                if "events" in iData["rawCalendar"].keys():
4119                    for item in iData["rawCalendar"]["events"]:
4120                        calendarData.append({
4121                            "couponDate": item["couponDate"],
4122                            "couponNumber": int(item["couponNumber"]),
4123                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4124                            "payCurrency": item["payOneBond"]["currency"],
4125                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4126                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4127                            "couponStartDate": item["couponStartDate"],
4128                            "couponEndDate": item["couponEndDate"],
4129                            "couponPeriod": item["couponPeriod"],
4130                        })
4131
4132                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4133                    if "maturityDate" not in iData.keys():
4134                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4135
4136                # Widen raw data with Coupon Rate.
4137                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4138                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4139                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4140                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4141
4142                # Widen raw data with Yield to Maturity (YTM) on current date.
4143                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4144                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4145                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4146                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4147                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4148                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4149
4150                iData["calendar"] = calendarData  # adds calendar at the end
4151
4152                # Remove not used data:
4153                iData.pop("uid")
4154                iData.pop("positionUid")
4155                iData.pop("currentPrice")
4156                iData.pop("rawCalendar")
4157
4158                colNames = list(iData.keys())
4159                if bonds is None:
4160                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4161
4162                else:
4163                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4164
4165            else:
4166                uLogger.warning("Instrument is not a bond!")
4167
4168            processed = round(100 * (i + 1) / iCount, 1)
4169            if tooLong and processed % 5 == 0:
4170                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4171
4172            else:
4173                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4174
4175        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4176
4177        # Saving bonds from Pandas DataFrame to XLSX sheet:
4178        if xlsx and self.bondsXLSXFile:
4179            with pd.ExcelWriter(
4180                    path=self.bondsXLSXFile,
4181                    date_format=TKS_DATE_FORMAT,
4182                    datetime_format=TKS_DATE_TIME_FORMAT,
4183                    mode="w",
4184            ) as writer:
4185                bonds.to_excel(
4186                    writer,
4187                    sheet_name="Extended bonds data",
4188                    index=True,
4189                    encoding="UTF-8",
4190                    freeze_panes=(1, 1),
4191                )  # saving as XLSX-file with freeze first row and column as headers
4192
4193            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4194
4195        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4197    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4198        """
4199        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4200
4201        WARNING! This is too long operation if a lot of bonds requested from broker server.
4202
4203        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4204
4205        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4206                        extended information about bonds: main info, current prices, bond payment calendar,
4207                        coupon yields, current yields and some statistics etc.
4208                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4209        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4210                     for further used by data scientists or stock analytics.
4211        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4212        """
4213        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4214            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4215
4216        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4217
4218        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4219        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4220        calendar = None
4221        for bond in extBonds.iterrows():
4222            for item in bond[1]["calendar"]:
4223                cData = {
4224                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4225                    "couponDate": item["couponDate"],
4226                    "figi": bond[1]["figi"],
4227                    "ticker": bond[1]["ticker"],
4228                    "name": bond[1]["name"],
4229                    "couponNumber": item["couponNumber"],
4230                    "payOneBond": item["payOneBond"],
4231                    "payCurrency": item["payCurrency"],
4232                    "couponType": item["couponType"],
4233                    "couponPeriod": item["couponPeriod"],
4234                    "fixDate": item["fixDate"],
4235                    "couponStartDate": item["couponStartDate"],
4236                    "couponEndDate": item["couponEndDate"],
4237                }
4238
4239                if calendar is None:
4240                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4241
4242                else:
4243                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4244
4245        if calendar is not None:
4246            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4247
4248            # Saving calendar from Pandas DataFrame to XLSX sheet:
4249            if xlsx:
4250                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4251
4252                with pd.ExcelWriter(
4253                        path=xlsxCalendarFile,
4254                        date_format=TKS_DATE_FORMAT,
4255                        datetime_format=TKS_DATE_TIME_FORMAT,
4256                        mode="w",
4257                ) as writer:
4258                    humanReadable = calendar.copy(deep=True)
4259                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4260                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4261                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4262                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4263                    humanReadable.columns = colNames  # human-readable column names
4264
4265                    humanReadable.to_excel(
4266                        writer,
4267                        sheet_name="Bond payments calendar",
4268                        index=False,
4269                        encoding="UTF-8",
4270                        freeze_panes=(1, 2),
4271                    )  # saving as XLSX-file with freeze first row and column as headers
4272
4273                    del humanReadable  # release df in memory
4274
4275                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4276
4277        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4279    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4280        """
4281        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4282        Also, creates Markdown file with calendar data, `calendar.md` by default.
4283
4284        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4285
4286        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4287                        extended information about bonds: main info, current prices, bond payment calendar,
4288                        coupon yields, current yields and some statistics etc.
4289                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4290        :param show: if `True` then also printing bonds payment calendar to the console,
4291                     otherwise save to file `calendarFile` only. `False` by default.
4292        :return: multilines text in Markdown format with bonds payment calendar as a table.
4293        """
4294        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4295            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4296
4297        infoText = "# Bond payments calendar\n\n"
4298
4299        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4300
4301        if not (calendar is None or calendar.empty):
4302            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4303
4304            info = [
4305                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4306                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4307            ]
4308
4309            newMonth = False
4310            notOneBond = calendar["figi"].nunique() > 1
4311            for i, bond in enumerate(calendar.iterrows()):
4312                if newMonth and notOneBond:
4313                    info.append(splitLine)
4314
4315                info.append(
4316                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4317                        "  √" if bond[1]["paid"] else "  —",
4318                        bond[1]["couponDate"].split("T")[0],
4319                        bond[1]["figi"],
4320                        bond[1]["ticker"],
4321                        bond[1]["couponNumber"],
4322                        "{} {}".format(
4323                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4324                            bond[1]["payCurrency"],
4325                        ),
4326                        bond[1]["couponType"],
4327                        bond[1]["couponPeriod"],
4328                        bond[1]["fixDate"].split("T")[0],
4329                    )
4330                )
4331
4332                if i < len(calendar.values) - 1:
4333                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4334                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4335                    newMonth = False if curDate.month == nextDate.month else True
4336
4337                else:
4338                    newMonth = False
4339
4340            infoText += "".join(info)
4341
4342            if show:
4343                uLogger.info("{}".format(infoText))
4344
4345            if self.calendarFile is not None:
4346                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4347                    fH.write(infoText)
4348
4349                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4350
4351        else:
4352            infoText += "No data\n"
4353
4354        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4356    def OverviewAccounts(self, show: bool = False) -> dict:
4357        """
4358        Method for parsing and show simple table with all available user accounts.
4359
4360        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4361
4362        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4363        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4364                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4365                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4366                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4367                                                        "closed": "—", "access": "Full access" }, ...}}`
4368        """
4369        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4370
4371        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4372        accounts = {
4373            item["id"]: {
4374                "type": TKS_ACCOUNT_TYPES[item["type"]],
4375                "name": item["name"],
4376                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4377                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4378                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4379                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4380            } for item in rawAccounts["accounts"]
4381        }
4382
4383        # Raw and parsed data with some fields replaced in "stat" section:
4384        view = {
4385            "rawAccounts": rawAccounts,
4386            "stat": accounts,
4387        }
4388
4389        # --- Prepare simple text table with only accounts data in human-readable format:
4390        if show:
4391            info = [
4392                "# User accounts\n\n",
4393                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4394                "| Account ID   | Type                      | Status                    | Name                           |\n",
4395                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4396            ]
4397
4398            for account in view["stat"].keys():
4399                info.extend([
4400                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4401                        account,
4402                        view["stat"][account]["type"],
4403                        view["stat"][account]["status"],
4404                        view["stat"][account]["name"],
4405                    )
4406                ])
4407
4408            infoText = "".join(info)
4409
4410            uLogger.info(infoText)
4411
4412            if self.userAccountsFile:
4413                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4414                    fH.write(infoText)
4415
4416                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4417
4418        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4420    def OverviewUserInfo(self, show: bool = False) -> dict:
4421        """
4422        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4423
4424        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4425
4426        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4427        :return: dict with raw parsed data from server and some calculated statistics about it.
4428        """
4429        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4430        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4431        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4432        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4433        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4434        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4435
4436        # This is dict with parsed common user data:
4437        userInfo = {
4438            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4439            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4440            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4441            "tariff": rawUserInfo["tariff"],
4442        }
4443
4444        # This is an array of dict with parsed margin statuses for every account IDs:
4445        margins = {}
4446        for accountId in accounts.keys():
4447            if rawMargins[accountId]:
4448                margins[accountId] = {
4449                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4450                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4451                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4452                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4453                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4454                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4455                }
4456
4457            else:
4458                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4459
4460        unary = {}  # unary-connection limits
4461        for item in rawTariffLimits["unaryLimits"]:
4462            if item["limitPerMinute"] in unary.keys():
4463                unary[item["limitPerMinute"]].extend(item["methods"])
4464
4465            else:
4466                unary[item["limitPerMinute"]] = item["methods"]
4467
4468        stream = {}  # stream-connection limits
4469        for item in rawTariffLimits["streamLimits"]:
4470            if item["limit"] in stream.keys():
4471                stream[item["limit"]].extend(item["streams"])
4472
4473            else:
4474                stream[item["limit"]] = item["streams"]
4475
4476        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4477        limits = {
4478            "unary": unary,
4479            "stream": stream,
4480        }
4481
4482        # Raw and parsed data as an output result:
4483        view = {
4484            "rawUserInfo": rawUserInfo,
4485            "rawAccounts": rawAccounts,
4486            "rawMargins": rawMargins,
4487            "rawTariffLimits": rawTariffLimits,
4488            "stat": {
4489                "userInfo": userInfo,
4490                "accounts": accounts,
4491                "margins": margins,
4492                "limits": limits,
4493            },
4494        }
4495
4496        # --- Prepare text table with user information in human-readable format:
4497        if show:
4498            info = [
4499                "# Full user information\n\n",
4500                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4501                "## Common information\n\n",
4502                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4503                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4504                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4505                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4506                "\n## User accounts\n\n",
4507            ]
4508
4509            for account in view["stat"]["accounts"].keys():
4510                info.extend([
4511                    "### ID: [{}]\n\n".format(account),
4512                    "| Parameters           | Values                                                       |\n",
4513                    "|----------------------|--------------------------------------------------------------|\n",
4514                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4515                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4516                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4517                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4518                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4519                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4520                ])
4521
4522                if margins[account]:
4523                    info.extend([
4524                        "| Margin status:       | Enabled                                                      |\n",
4525                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4526                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4527                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4528                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4529                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4530                    ])
4531
4532                else:
4533                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4534
4535            info.extend([
4536                "\n## Current user tariff limits\n",
4537                "\nSee also:\n",
4538                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4539                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4540                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4541                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4542                "\n### Unary limits\n",
4543            ])
4544
4545            if unary:
4546                for key, values in sorted(unary.items()):
4547                    info.append("\n* Max requests per minute: {}\n".format(key))
4548
4549                    for value in values:
4550                        info.append("  - {}\n".format(value))
4551
4552            else:
4553                info.append("\nNot available\n")
4554
4555            info.append("\n### Stream limits\n")
4556
4557            if stream:
4558                for key, values in sorted(stream.items()):
4559                    info.append("\n* Max stream connections: {}\n".format(key))
4560
4561                    for value in values:
4562                        info.append("  - {}\n".format(value))
4563
4564            else:
4565                info.append("\nNot available\n")
4566
4567            infoText = "".join(info)
4568
4569            uLogger.info(infoText)
4570
4571            if self.userInfoFile:
4572                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4573                    fH.write(infoText)
4574
4575                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4576
4577        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4580class Args:
4581    """
4582    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4583    """
4584    def __init__(self, **kwargs):
4585        self.__dict__.update(kwargs)
4586
4587    def __getattr__(self, item):
4588        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4584    def __init__(self, **kwargs):
4585        self.__dict__.update(kwargs)
def ParseArgs()
4591def ParseArgs():
4592    """This function get and parse command line keys."""
4593    parser = ArgumentParser()  # command-line string parser
4594
4595    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4596    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4597
4598    # --- options:
4599
4600    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4601    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4602    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4603
4604    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4605    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4606
4607    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4608    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4609
4610    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4611
4612    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4613    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4614    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4615
4616    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4617    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4618
4619    # --- commands:
4620
4621    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4622
4623    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4624    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4625    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4626    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4627    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4628    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4629    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4630    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4631
4632    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4633    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4634    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4635    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4636    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4637    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4638
4639    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4640    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4641    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4642    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4643
4644    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4645    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4646    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4647
4648    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4649    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4650    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4651    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4652    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4653    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4654    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4655
4656    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4657    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4658    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4659    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4660    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4661
4662    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4663    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4664    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4665
4666    cmdArgs = parser.parse_args()
4667    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4670def Main(**kwargs):
4671    """
4672    Main function for work with TKSBrokerAPI in the console.
4673
4674    See examples:
4675    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4676    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4677    """
4678    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4679
4680    if args.debug_level:
4681        uLogger.level = 10  # always debug level by default
4682        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4683
4684    exitCode = 0
4685    start = datetime.now(tzutc())
4686    uLogger.debug("=-" * 50)
4687    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4688        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4689        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4690    ))
4691
4692    # trying to calculate full current version:
4693    buildVersion = __version__
4694    try:
4695        v = version("tksbrokerapi")
4696        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4697
4698    except Exception:
4699        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4700
4701    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4702    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4703
4704    try:
4705        if args.version:
4706            print("TKSBrokerAPI {}".format(buildVersion))
4707            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4708
4709        else:
4710            # Init class for trading with Tinkoff Broker:
4711            trader = TinkoffBrokerServer(
4712                token=args.token,
4713                accountId=args.account_id,
4714                useCache=not args.no_cache,
4715            )
4716
4717            # --- set some options:
4718
4719            if args.more:
4720                trader.moreDebug = True
4721                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4722
4723            if args.ticker:
4724                ticker = args.ticker.upper()  # Tickers may be upper case only
4725
4726                if ticker in trader.aliasesKeys:
4727                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4728
4729                else:
4730                    trader.ticker = ticker
4731
4732            if args.figi:
4733                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4734
4735            if args.depth is not None:
4736                trader.depth = args.depth
4737
4738            # --- do one command:
4739
4740            if args.list:
4741                if args.output is not None:
4742                    trader.instrumentsFile = args.output
4743
4744                trader.ShowInstrumentsInfo(show=True)
4745
4746            elif args.list_xlsx:
4747                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4748
4749            elif args.bonds_xlsx is not None:
4750                if args.output is not None:
4751                    trader.bondsXLSXFile = args.output
4752
4753                if len(args.bonds_xlsx) == 0:
4754                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4755
4756                else:
4757                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4758
4759            elif args.search:
4760                if args.output is not None:
4761                    trader.searchResultsFile = args.output
4762
4763                trader.SearchInstruments(pattern=args.search[0], show=True)
4764
4765            elif args.info:
4766                if not (args.ticker or args.figi):
4767                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4768                    raise Exception("Ticker or FIGI required")
4769
4770                if args.output is not None:
4771                    trader.infoFile = args.output
4772
4773                if args.ticker:
4774                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4775
4776                else:
4777                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4778
4779            elif args.calendar is not None:
4780                if args.output is not None:
4781                    trader.calendarFile = args.output
4782
4783                if len(args.calendar) == 0:
4784                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4785
4786                else:
4787                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4788
4789                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4790
4791            elif args.price:
4792                if not (args.ticker or args.figi):
4793                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4794                    raise Exception("Ticker or FIGI required")
4795
4796                trader.GetCurrentPrices(show=True)
4797
4798            elif args.prices is not None:
4799                if args.output is not None:
4800                    trader.pricesFile = args.output
4801
4802                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4803
4804            elif args.overview:
4805                if args.output is not None:
4806                    trader.overviewFile = args.output
4807
4808                trader.Overview(show=True, details="full")
4809
4810            elif args.overview_digest:
4811                if args.output is not None:
4812                    trader.overviewDigestFile = args.output
4813
4814                trader.Overview(show=True, details="digest")
4815
4816            elif args.overview_positions:
4817                if args.output is not None:
4818                    trader.overviewPositionsFile = args.output
4819
4820                trader.Overview(show=True, details="positions")
4821
4822            elif args.overview_orders:
4823                if args.output is not None:
4824                    trader.overviewOrdersFile = args.output
4825
4826                trader.Overview(show=True, details="orders")
4827
4828            elif args.overview_analytics:
4829                if args.output is not None:
4830                    trader.overviewAnalyticsFile = args.output
4831
4832                trader.Overview(show=True, details="analytics")
4833
4834            elif args.overview_calendar:
4835                if args.output is not None:
4836                    trader.overviewAnalyticsFile = args.output
4837
4838                trader.Overview(show=True, details="calendar")
4839
4840            elif args.deals is not None:
4841                if args.output is not None:
4842                    trader.reportFile = args.output
4843
4844                if 0 <= len(args.deals) < 3:
4845                    trader.Deals(
4846                        start=args.deals[0] if len(args.deals) >= 1 else None,
4847                        end=args.deals[1] if len(args.deals) == 2 else None,
4848                        show=True,  # Always show deals report in console
4849                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4850                    )
4851
4852                else:
4853                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4854                    raise Exception("Incorrect value")
4855
4856            elif args.history is not None:
4857                if args.output is not None:
4858                    trader.historyFile = args.output
4859
4860                if 0 <= len(args.history) < 3:
4861                    dataReceived = trader.History(
4862                        start=args.history[0] if len(args.history) >= 1 else None,
4863                        end=args.history[1] if len(args.history) == 2 else None,
4864                        interval="hour" if args.interval is None or not args.interval else args.interval,
4865                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4866                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4867                        show=True,  # shows all downloaded candles in console
4868                    )
4869
4870                    if args.render_chart is not None and dataReceived is not None:
4871                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4872
4873                        trader.ShowHistoryChart(
4874                            candles=dataReceived,
4875                            interact=iChart,
4876                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4877                        )
4878
4879                else:
4880                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4881                    raise Exception("Incorrect value")
4882
4883            elif args.load_history is not None:
4884                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4885
4886                if args.render_chart is not None and histData is not None:
4887                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4888                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4889
4890                    trader.ShowHistoryChart(
4891                        candles=histData,
4892                        interact=iChart,
4893                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4894                    )
4895
4896            elif args.trade is not None:
4897                if 1 <= len(args.trade) <= 5:
4898                    trader.Trade(
4899                        operation=args.trade[0],
4900                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4901                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4902                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4903                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4904                    )
4905
4906                else:
4907                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4908
4909            elif args.buy is not None:
4910                if 0 <= len(args.buy) <= 4:
4911                    trader.Buy(
4912                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4913                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4914                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4915                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4916                    )
4917
4918                else:
4919                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4920
4921            elif args.sell is not None:
4922                if 0 <= len(args.sell) <= 4:
4923                    trader.Sell(
4924                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4925                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4926                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4927                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4928                    )
4929
4930                else:
4931                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4932
4933            elif args.order:
4934                if 4 <= len(args.order) <= 7:
4935                    trader.Order(
4936                        operation=args.order[0],
4937                        orderType=args.order[1],
4938                        lots=int(args.order[2]),
4939                        targetPrice=float(args.order[3]),
4940                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4941                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4942                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4943                    )
4944
4945                else:
4946                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4947
4948            elif args.buy_limit:
4949                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4950
4951            elif args.sell_limit:
4952                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4953
4954            elif args.buy_stop:
4955                if 2 <= len(args.buy_stop) <= 7:
4956                    trader.BuyStop(
4957                        lots=int(args.buy_stop[0]),
4958                        targetPrice=float(args.buy_stop[1]),
4959                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4960                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4961                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4962                    )
4963
4964                else:
4965                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4966
4967            elif args.sell_stop:
4968                if 2 <= len(args.sell_stop) <= 7:
4969                    trader.SellStop(
4970                        lots=int(args.sell_stop[0]),
4971                        targetPrice=float(args.sell_stop[1]),
4972                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4973                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4974                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4975                    )
4976
4977                else:
4978                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4979
4980            # elif args.buy_order_grid is not None:
4981            #     # update order grid work with api v2
4982            #     if len(args.buy_order_grid) == 2:
4983            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4984            #
4985            #         for order in orderParams:
4986            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4987            #
4988            #     else:
4989            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4990            #
4991            # elif args.sell_order_grid is not None:
4992            #     # update order grid work with api v2
4993            #     if len(args.sell_order_grid) >= 2:
4994            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4995            #
4996            #         for order in orderParams:
4997            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4998            #
4999            #     else:
5000            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5001
5002            elif args.close_order is not None:
5003                trader.CloseOrders(args.close_order)  # close only one order
5004
5005            elif args.close_orders is not None:
5006                trader.CloseOrders(args.close_orders)  # close list of orders
5007
5008            elif args.close_trade:
5009                if not (args.ticker or args.figi):
5010                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5011                    raise Exception("Ticker or FIGI required")
5012
5013                if args.ticker:
5014                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
5015
5016                else:
5017                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
5018
5019            elif args.close_trades is not None:
5020                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5021
5022            elif args.close_all is not None:
5023                if args.ticker:
5024                    trader.CloseAllByTicker(instrument=args.ticker)
5025
5026                elif args.figi:
5027                    trader.CloseAllByFIGI(instrument=args.figi)
5028
5029                else:
5030                    trader.CloseAll(*args.close_all)
5031
5032            elif args.limits:
5033                if args.output is not None:
5034                    trader.withdrawalLimitsFile = args.output
5035
5036                trader.OverviewLimits(show=True)
5037
5038            elif args.user_info:
5039                if args.output is not None:
5040                    trader.userInfoFile = args.output
5041
5042                trader.OverviewUserInfo(show=True)
5043
5044            elif args.account:
5045                if args.output is not None:
5046                    trader.userAccountsFile = args.output
5047
5048                trader.OverviewAccounts(show=True)
5049
5050            else:
5051                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5052                raise Exception("There is no command to execute")
5053
5054    except Exception:
5055        trace = tb.format_exc()
5056        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5057            if e in trace:
5058                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5059                break
5060
5061        uLogger.debug(trace)
5062        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5063        exitCode = 255  # an error occurred, must be open a ticket for this issue
5064
5065    finally:
5066        finish = datetime.now(tzutc())
5067
5068        if exitCode == 0:
5069            if args.more:
5070                uLogger.debug("All operations were finished success (summary code is 0).")
5071
5072        else:
5073            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5074                os.path.abspath(uLog.defaultLogFile), exitCode,
5075            ))
5076
5077        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5078        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5079            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5080            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5081        ))
5082        uLogger.debug("=-" * 50)
5083
5084        if not kwargs:
5085            sys.exit(exitCode)
5086
5087        else:
5088            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: